Password Confirm Angular Material - javascript

I'm working to authenticate a user with Angular Material. I'm currently trying to get the proper mat-error to display when the confirmation password doesn't match the first entered pw.
Here is my html:
<mat-form-field hintLabel="Minimum 8 Characters" class="">
<mat-label>Password</mat-label>
<input
matInput
#input
type="password"
formControlName="password">
<mat-hint align="end">{{input.value?.length || 0}}/8</mat-hint>
</mat-form-field>
<mat-form-field>
<mat-label>Confirm</mat-label>
<input
matInput
required
type="password"
#confirm
formControlName="confirmPassword">
<mat-error *ngIf="form.get('confirmPassword').invalid || confirmPasswordMismatch">Password does not match</mat-error>
</mat-form-field>
The error displays once the user has focused on password confirmation and unfocuses without entering anything. Once the user enters anything, the error goes away even though the confirmation doesn't match the password.
Here is my TS file:
public get confirmPasswordMismatch() {
return (this.form.get('password').dirty || this.form.get('confirmPassword').dirty) && this.form.hasError('confirmedDoesNotMatch');
}
this.form = new FormGroup({
userName: new FormControl(null, [Validators.required]),
fullName: new FormControl(null, [Validators.required]),
email: new FormControl(null, [Validators.required, Validators.pattern(this.EMAIL_REGEX)]),
password: new FormControl(null),
confirmPassword: new FormControl(null, ),
}, (form: FormGroup) => passwordValidator.validate(form));
The desired effect is that the error shows when the user has entered text into pw input when confirm pw is empty and to show an error when both have text but confirm doesn't match pw.

I solved it like this:
Template:
<mat-form-field>
<input matInput type="password" placeholder="Password" formControlName="password" (input)="onPasswordInput()">
<mat-error *ngIf="password.hasError('required')">Password is required</mat-error>
<mat-error *ngIf="password.hasError('minlength')">Password must have at least {{minPw}} characters</mat-error>
</mat-form-field>
<mat-form-field>
<input matInput type="password" placeholder="Confirm password" formControlName="password2" (input)="onPasswordInput()">
<mat-error *ngIf="password2.hasError('required')">Please confirm your password</mat-error>
<mat-error *ngIf="password2.invalid && !password2.hasError('required')">Passwords don't match</mat-error>
</mat-form-field>
Component:
export class SignUpFormComponent implements OnInit {
minPw = 8;
formGroup: FormGroup;
constructor(private formBuilder: FormBuilder) { }
ngOnInit() {
this.formGroup = this.formBuilder.group({
password: ['', [Validators.required, Validators.minLength(this.minPw)]],
password2: ['', [Validators.required]]
}, {validator: passwordMatchValidator});
}
/* Shorthands for form controls (used from within template) */
get password() { return this.formGroup.get('password'); }
get password2() { return this.formGroup.get('password2'); }
/* Called on each input in either password field */
onPasswordInput() {
if (this.formGroup.hasError('passwordMismatch'))
this.password2.setErrors([{'passwordMismatch': true}]);
else
this.password2.setErrors(null);
}
}
Validator:
export const passwordMatchValidator: ValidatorFn = (formGroup: FormGroup): ValidationErrors | null => {
if (formGroup.get('password').value === formGroup.get('password2').value)
return null;
else
return {passwordMismatch: true};
};
Notes:
Thanks to onPasswordInput() being called from either password field, editing the first password field (and thus invalidating the password confirmation) also causes the mismatch error to be displayed in the password confirmation field.
The *ngIf="password2.invalid && !password2.hasError('required')" test for the password confirmation field ensures that never both error messages ("mismatch" and "required") are displayed at the same time.

Based on your code, it doesn't look like you added any validation for the confirmPassword field: confirmPassword: new FormControl(null, ) so the only validation happening is via the required attribute. Also, the mat-error will only be displayed by the form field if the form control is invalid. That means you can't force an error to be displayed just by using ngIf. Since you only have required validation on that field, it makes sense that you only have an error when the field is empty. To solve this problem, you need to create a validator for mismatch checking and add it to the confirmPassword form control. As an alternative, you can manually add errors to the form control using setErrors() when the field changes by adding an input listener - for example (this is just from memory - may need fixing):
<mat-form-field>
<mat-label>Confirm</mat-label>
<input matInput required type="password" #confirm formControlName="confirmPassword"
(input)="onInput($event.target.value)">
<mat-error *ngIf="form.get('confirmPassword').invalid>
Password does not match
</mat-error>
</mat-form-field>
onInput(value) {
if (this.form.hasError('confirmedDoesNotMatch')) { // or some other test of the value
this.form.get('confirmPassword').setErrors([{'confirmedDoesNotMatch': true}]);
} else {
this.form.get('confirmPassword').setErrors(null);
}
}

you are essentially validating how 2 fields in a form interact with each other ("password" and "confirm password" fields). This is known as "cross-field validation"
that means, your custom validator cannot be assigned to just 1 field. The custom validator needs to be assigned to the common parent, the form.
Here is the official documented best practice, for validating how 2 fields in a form interact with each other
https://angular.io/guide/form-validation#cross-field-validation
this code snippet worked for me
template:
<form method="post" [formGroup]="newPasswordForm">
<input type="password" formControlName="newPassword" />
<input type="password" formControlName="newPasswordConfirm" />
<div class="err-msg" *ngIf="newPasswordForm.errors?.passwordMismatch && (newPasswordForm.touched || newPasswordForm.dirty)">
confirm-password does not match password
</div>
</form>
component.ts:
export class Component implements OnInit {
this.newPasswordForm = new FormGroup({
'newPassword': new FormControl('', [
Validators.required,
]),
'newPasswordConfirm': new FormControl('', [
Validators.required
])
}, { validators: passwordMatchValidator });
}
export const passwordMatchValidator: ValidatorFn = (formGroup: FormGroup): ValidationErrors | null => {
return formGroup.get('newPassword').value === formGroup.get('newPasswordConfirm').value ?
null : { 'passwordMismatch': true };
}
Note that for passwordMatchValidator, it is outside the component class. It is NOT inside the class

Henry's answer was very helpful (and I'm surprised it doesn't have more votes), but I've made a tweak to it so that it plays well with Angular Material controls.
The tweak relies on the fact that the FormGroup class has a parent property. Using this property, you can tie the validation message to the password confirmation field, and then refer up the chain in the validator.
public signUpFormGroup: FormGroup = this.formBuilder.group({
email: ['', [Validators.required, Validators.pattern(validation.patterns.email)]],
newPassword: this.formBuilder.group({
password: ['', [
Validators.required,
Validators.minLength(validation.passwordLength.min),
Validators.maxLength(validation.passwordLength.max)]],
confirmPassword: ['', [Validators.required, passwordMatchValidator]]
})
});
The validator looks like this:
export const passwordMatchValidator: ValidatorFn = (formGroup: FormGroup): ValidationErrors | null => {
const parent = formGroup.parent as FormGroup;
if (!parent) return null;
return parent.get('password').value === parent.get('confirmPassword').value ?
null : { 'mismatch': true };
}
and the form then looks like this:
<div formGroupName="newPassword" class="full-width new-password">
<mat-form-field class="full-width sign-up-password">
<mat-label>{{ 'sign-up.password' | translate }}</mat-label>
<input matInput [type]="toggleSignUpPass.type" [maxlength]="max" [minlength]="min" formControlName="password" required/>
<mat-pass-toggle-visibility #toggleSignUpPass matSuffix></mat-pass-toggle-visibility>
<mat-icon matSuffix [color]="color$ | async">lock</mat-icon>
<mat-hint aria-live="polite">{{ signUpFormGroup.get('newPassword').value.password.length }}
/ {{ max }} </mat-hint>
<mat-error *ngIf="signUpFormGroup?.get('newPassword')?.controls?.password?.hasError('required')">
{{ 'sign-up.password-error.required' | translate }}
</mat-error>
<mat-error class="password-too-short" *ngIf="signUpFormGroup?.get('newPassword')?.controls?.password?.hasError('minlength')">
{{ 'sign-up.password-error.min-length' | translate:passwordRestriction.minLength }}
</mat-error>
<mat-error *ngIf="signUpFormGroup?.get('newPassword')?.controls?.password?.hasError('maxlength')">
{{ 'sign-up.password-error.max-length' | translate:passwordRestriction.maxLength }}
</mat-error>
</mat-form-field>
<mat-form-field class="full-width sign-up-confirm-password">
<mat-label>{{ 'sign-up.confirm-password' | translate }}</mat-label>
<input matInput [type]="toggleSignUpPassConf.type" formControlName="confirmPassword" required />
<mat-pass-toggle-visibility #toggleSignUpPassConf matSuffix></mat-pass-toggle-visibility>
<mat-icon matSuffix [color]="color$ | async">lock</mat-icon>
<mat-error *ngIf="signUpFormGroup?.get('newPassword')?.controls?.confirmPassword?.hasError('required')">
{{ 'sign-up.confirm-password-error.required' | translate }}
</mat-error>
<mat-error class="password-mismatch" *ngIf="signUpFormGroup?.get('newPassword')?.controls?.confirmPassword?.hasError('mismatch')">
{{ 'sign-up.password-error.mismatch' | translate }}
</mat-error>
</mat-form-field>
</div>

Related

Angular forms require one of two form groups to be valid

I am trying to implement a reactive angular form where either A or B has to be entered. A is a unique id and B is a set of values which identify the id. Now I try to validate a Form that is valid if either A is entered or B is entered including all the required values. I found several solutions that implement this behavior based on FormFields but was not able to get it working with the group of values.
<form class="container" [formGroup]="myForm" (ngSubmit)="onSubmit()">
<mat-form-field class="w-1/2">
<mat-label>ID</mat-label>
<input matInput type="number" formControlName="id">
</mat-form-field>
<div class="grid grid-cols-3 gap-4" formGroupName="innerGroup">
<mat-form-field>
<mat-label>First Name</mat-label>
<input matInput type="number" formControlName="firstName">
</mat-form-field>
<mat-form-field>
<mat-label>Last Name</mat-label>
<input matInput type="number" formControlName="lastName">
</mat-form-field>
</div>
</form>
My first idea was to override the default validator for the form but I could not figure out how to do that. Not even sure if it would be possible. I was trying to adjust https://stackoverflow.com/a/48714721 to work in my scenario but I had no idea how to get it to work because of the additional complexity with the inner form group.
Using angular 14 I was able to produce a similar result to what you are describing I am not sure it will 100% solve your issue however it might help.
Basically what I did was create a validator function that is to be applied at the group level. This validator will check the valid state of any given controls be they a FormGroup or a FormControl. However, this alone will not solve the problem as if you have a form group angular will see that any underling control or group that is invalid will also invalidate the parent. So what I did was call .disable() on any control that was being checked by the validator function. This will disable the UI element and disable validation checking by angular allowing the parent to be considered valid when one of the children is valid but the other is invalid effectively creating a one and only one validator.
My specific example I was trying to get the OnlyOneValidator to work for a MatStepper.
Validator
export function onlyOneValidator(controlKeys: string[]) {
return (control: AbstractControl): ValidationErrors | null => {
let countOfValidControls = 0;
for (let key of controlKeys) {
const controlToCheck = control.get(key);
if (controlToCheck === null || controlToCheck === undefined) {
throw new Error(`Error: Invalid control key specified key was ${key}`);
}
countOfValidControls += controlToCheck?.valid ? 1 : 0;
}
if (countOfValidControls !== 1) {
// the count is not exactly one
return {
onlyOneValid: {
actualValidCount: countOfValidControls,
expectedValidCount: 1
}
};
}
return null;
};
}
Controller
#Component({
selector: "app-equipment-creation-page",
templateUrl: "./equipment-creation-page.component.html",
styleUrls: ["./equipment-creation-page.component.scss"],
})
export class EquipmentCreationPageComponent implements OnInit, OnDestroy {
public categories = [null, "Tools", "Vehicles"];
constructor(private _formBuilder: FormBuilder) {}
public categoryInformationGroup = this._formBuilder.group({
existingCategory: this._formBuilder.group({
category: new FormControl(null, [ Validators.required ])
}),
newCategory: this._formBuilder.group({
name: new FormControl("", [Validators.required]),
description: new FormControl("", [Validators.required])
})
}, {
validators: [
onlyOneValidator(["existingCategory", "newCategory"])
],
});
public ngOnDestroy(): void {
this.subscriptions.forEach(sub => {
sub.unsubscribe();
});
}
private subscriptions: Subscription[] = [];
public ngOnInit(): void {
this.subscriptions.push(this.categoryInformationGroup.controls.existingCategory.statusChanges.pipe(
tap((status: string) => {
if (status === "VALID") {
this.categoryInformationGroup.controls.newCategory.disable();
} else {
this.categoryInformationGroup.controls.newCategory.enable();
}
})
).subscribe());
this.subscriptions.push(this.categoryInformationGroup.controls.newCategory.statusChanges.pipe(
tap((status: string) => {
if (status === "VALID") {
this.categoryInformationGroup.controls.existingCategory.disable();
} else {
this.categoryInformationGroup.controls.existingCategory.enable();
}
})
).subscribe());
}
}
Template
<form [formGroup]="categoryInformationGroup.controls.existingCategory">
<mat-form-field>
<mat-label>Apply to existing category?</mat-label>
<mat-select formControlName="category">
<mat-option *ngFor="let category of categories" [value]="category">
{{ category ?? "None" }}
</mat-option>
</mat-select>
</mat-form-field>
</form>
OR
<form [formGroup]="categoryInformationGroup.controls.newCategory">
<mat-form-field>
<mat-label>Create New Category</mat-label>
<input matInput formControlName="name" placeholder="Name">
<mat-error *ngIf="categoryInformationGroup.controls.newCategory.controls.name.hasError('required')">This field
is required
</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>Create New Category</mat-label>
<input matInput formControlName="description" placeholder="Description">
<mat-error *ngIf="categoryInformationGroup.controls.newCategory.controls.description.hasError('required')">
This field is required
</mat-error>
</mat-form-field>
</form>
Hopefully this helps or at least gives you some ideas about how to approach this. If anyone else has any thoughts on this please let me know I would love to find a better way to do this.

Disable Angular 5 Input fields correct way

I have a FormGroup that was created like that:
form: FormGroup;
constructor(private _formBuilder: FormBuilder) { }
this.form = this._formBuilder.group({
name: ['', Validators.required],
email: ['', Validators.required, Validators.email]
});
When an event occurs I want to disable those inputs, so, in the HTML I added:
<input class="form-control" placeholder="Name" name="name" formControlName="name" [(ngModel)]="name" autocomplete="off" [disabled]="isDisabled" required>
<input class="form-control" placeholder="Email" name="email" formControlName="email" [(ngModel)]="email" email="true" autocomplete="off" [disabled]="isDisabled" required>
Where isDisabled is a variable I toggle to true when the said event happens.
As you can imagine, I get the message:
It looks like you're using the disabled attribute with a reactive form
directive. If you set disabled to true
when you set up this control in your component class, the disabled attribute will actually be set in the DOM for
you. We recommend using this approach to avoid 'changed after checked' errors.
Example:
form = new FormGroup({
first: new FormControl({value: 'Nancy', disabled: true}, Validators.required),
last: new FormControl('Drew', Validators.required)
});
So, with the example this warning shows and with a little search I found that I should declare my controls like:
name: [{ value: '', disabled: this.isDisabled }, Validators.required]
The problem is: It is not toggling between disabled/enabled when the variable changes between true/false
How is the correct way of having a variable controlling if an input is enabled or disabled?
I don't want to do it manually (ex: this.form.controls['name'].disable()) because it doesn't seems a very reactive way, I would have to call it inside a good amount of methods. Probably not a good practice.
Thx
You can change the assignment of the variable to a setter method so that you'd have:
set isDisabled(value: boolean) {
this._isDisabled = value;
if(value) {
this.form.controls['name'].disable();
} else {
this.form.controls['name'].enable();
}
}
One solution is creating a directive and using binding for that as described in here
import { NgControl } from '#angular/forms';
#Directive({
selector: '[disableControl]'
})
export class DisableControlDirective {
#Input() set disableControl( condition : boolean ) {
const action = condition ? 'disable' : 'enable';
this.ngControl.control[action]();
}
constructor( private ngControl : NgControl ) {
}
}
then
<input class="form-control" placeholder="Name" name="name" formControlName="name" autocomplete="off" [disableControl]="isDisabled" required>
NOTE:
Doesn't work with Ivy
For input use [readonly] rather than [disabled] and it'll work
The proper way to disable an form control. With reactive forms you should never disable an input from the template. So in whatever method in your component you are calling you should disable the input like this:
this.form.get('name').disable();
Disable TextBox in Angular 7
<div class="center-content tp-spce-hdr">
<div class="container">
<div class="row mx-0 mt-4">
<div class="col-12" style="padding-right: 700px;" >
<div class="form-group">
<label>Email</label>
<input [disabled]="true" type="text" id="email" name="email"
[(ngModel)]="email" class="form-control">
</div>
</div>
</div>
</div>
You can use this code on your ts file.
All controls:
this.form.disable()
this.form.enable()
Some controls
this.form.get('first').disable()
this.form.get('first').enable()
Or Initial set method.
first: new FormControl({value: '', disabled: true}, Validators.required)
In Reactive Form you can disable all form fields by this.form.disable().
In Template Driven Form you can disable all form fields by this.myform.form.disable() where myForm is #ViewChild('form') myForm;
Not the clean or dry'st I imagine. Bu I tried the "set method" and didn't work out of the box...
Needed some refactoring () => {simpleVersion} (hope it helps someone)
component.ts
...
// standard stuff...
form: FormGroup;
isEditing = false;
...
// build the form...
buildForm() {
this.form = this.FormBuilder.group({
key: [{value:'locked', disabled: !this.isEditing}],
name: [],
item: [],
active: [false]
})
}
// map the controls to "this" object
// => i.e. now you can refer to the controls directly (ex. this.yourControlName)
get key() { return <FormControl>this.form.get('key') }
get name() { return <FormControl>this.form.get('name') }
...
// ----------------------------------------
// THE GRAND FINALÉ - disable entire form or individual controls
// ----------------------------------------
toggleEdit() {
if(!this.isEditing) {
this.key.enable(); // controls
this.name.enable();
// this.form.enable(); // the form
this.isEditing = !this.isEditing;
} else {
this.key.disable(); // the controls
this.name.disable(); // the controls
// this.form.disable(); // or the entire form
this.isEditing = !this.isEditing;
}
}
& perhaps overkill on the HTML logic, so hope you find the bonus integrated ngClass toggle just as helpful.
component.html (toggle button)
<div class="btn-group" (click)="toggleEdit()">
<label
class="btn"
role="button"
[ngClass]="{'btn-success': isEditing,
'btn-warning': !isEditing}">toggle edit
</label>
</div>
The solution by creating a directive and using binding for that worked for me in Angular 10 is described in here
Template:
<mat-form-field>
<input matInput class="form-control" formControlName="NameText" [disableControl]="condition" type="text">
</mat-form-field>
TypeScript:
import { Directive, Input } from '#angular/core';
import { NgControl } from '#angular/forms';
#Directive({
selector: '[opDisabled]'
})
export class DisabledDirective {
#Input()
set opDisabled(condition: boolean) {
const action = condition ? 'disable' : 'enable';
setTimeout(() => this.ngControl.control[action]());
}
constructor(private ngControl: NgControl) {}
}
I have a function that enables a control on click.
controlClick(control: any) {
this.form.controls[control.ngControl.name].enable();
}
Originally i was using
control.disabled = false;
But this did not work for controls with <input> for example in my mat-chip-list.
I use FormGroup and disable each control in the constructor
constructor(
private fb: FormBuilder,
private dialogRef: MatDialogRef<EditDialogComponent>,
#Inject(MAT_DIALOG_DATA) data
) {
this.data = data;
this.multiEdit = data.multiSelect;
this.form = new FormGroup({
autoArchive: new FormControl({
value:
this.getPreFill(data.selectedPolicy.autoArchive, this.multiEdit),
disabled: true
/*, Validators.required*/
}),
...
<mat-form-field (click)="controlClick(retrieveChipList)">
<mat-chip-list #retrieveChipList formControlName="retrieveChipList">
<mat-chip
*ngFor="let email of data.selectedPolicy.retrieveEmailsToBeNotified"
(removed)="remove(email)" [selectable]="selectable"
[removable]="removable"
>
{{ email }}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input
placeholder="Retrieve Emails to be Notified"
formControlName="retrieveChipList"
[matChipInputFor]="retrieveChipList"
[matChipInputAddOnBlur]="true"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
(matChipInputTokenEnd)="addRetrieveEmails($event)"
/>
</mat-chip-list>
</mat-form-field>
As control can't be accessed in reactive forms. This is due to migration to Ivy. You can use can access the html attribute directly and specify your condition. See this issue #35330 for more details and alternative methods.
[attr.disabled]="true || false"
You can create set and get method to achieve conditionally enable/disable functionality for Angular material Reactive Forms:
*// 1st Step:
set isDisabled(value:boolean) {
if(value){
this.form.controls['Form controller name'].disable(); //you can keep empty if you don't add controller name
}
else{
this.form.controls['Form controller name'].enable();
}
}
// 2nd Step: Add conditions in getter
get isDisabled(){
return condition ? true : false;
}
// 3rd Step
this.form = this._formBuilder.group({
name: [{value: '', disabled: this.isDisabled }, [Validators.required]],
});
Remove [disabled]="isDisabled" from input fields and add ng-disabled="all" and in the event field add ng-model="all"
<body ng-app="">
Click here to disable all the form fields:<input type="checkbox" ng-model="all">
<input class="form-control" placeholder="Name" name="name" formControlName="name" [(ngModel)]="name" autocomplete="off" ng-disabled="all" required>
<input class="form-control" placeholder="Email" name="email" formControlName="email" [(ngModel)]="email" email="true" autocomplete="off" ng-disabled="all" required>
</body>

Form validation is not working in angular?

I want to check whether the dropdown is empty.
Need to show the required message and
If not empty, enable the submit button.
If empty, disable the submit button. Below is my html
Below is my html
<form [formGroup]="myForm" (ngSubmit)="save()" >
<mat-form-field>
<mat-select formControlName="name" placeholder="Element List" (selectionChange)="elementSelectionChange($event)" required>
<mat-option *ngFor="let element of Elements" [value]="element.name">
{{ element.name }}
</mat-option>
</mat-select>
<mat-error *ngIf="myForm.hasError('required', 'name')">Please choose an name</mat-error>
</mat-form-field>
<mat-form-field>
<mat-select formControlName="symbol" placeholder="Symbol List" required>
<mat-option *ngFor="let element of selectedElementSymbol" [value]="element.symbol">
{{ element.symbol }}
</mat-option>
</mat-select>
<mat-error *ngIf="myForm.hasError('required', 'symbol')">Please choose an symbol</mat-error>
</mat-form-field>
<div mat-dialog-actions>
<button mat-button (click)="onNoClick()">Cancel</button>
<button type="submit" mat-button cdkFocusInitial>Add</button>
</div>
</form>
below is my component
export class DialogOverviewExampleDialog {
myForm: FormGroup;
symbol = new FormControl('', Validators.required);
name = new FormControl('', Validators.required);
constructor(
public dialogRef: MatDialogRef<DialogOverviewExampleDialog>,
#Inject(MAT_DIALOG_DATA) public data: any,
private formBuilder: FormBuilder) {
this.myForm = this.formBuilder.group({
name: [this.name],
symbol: [this.symbol],
});
}
save() {
console.log(this.myForm.value);
}
}
updated demo here
You are currently assigning formcontrols to your formcontrol, whereas you want to assign value to your form controls. Below you are assigning form control name to formcontrol name:
WRONG:
name = new FormControl('', Validators.required);
this.myForm = this.formBuilder.group({
'name': [this.name, Validators.required],
// ...
});
so instead, just declare name, do what you want with the value, then assign that value to your form control...
CORRECT:
name: string;
this.myForm = this.formBuilder.group({
'name': [this.name, Validators.required],
// ...
});
Then just add [disabled]="!myForm.valid" on your submit button.
As for the other question, by default Angular material doesn't show error message unless the field has been touched, so you need to have a custom error state matcher that shows the error even when field is not touched (if that is what you want):
import {ErrorStateMatcher} from '#angular/material/core';
export class MyErrorStateMatcher implements ErrorStateMatcher {
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
return !!(control.invalid);
}
}
and in your TS, declare a error state matcher:
matcher = new MyErrorStateMatcher();
and use in template:
<mat-select formControlName="name" ... [errorStateMatcher]="matcher">
Here's your
StackBlitz
To make the submit button disabled (link)
<button type="submit" [disabled]="!myForm.valid" mat-button cdkFocusInitial>Add</button>
In order to check whether the dropdown is empty or not, you need to make the form fields required like
this.myForm = this.formBuilder.group({
'name': [this.name, Validators.required],
'symbol': [this.symbol, [Validators.required]]
});
Inorder to show the error highlight you need to add an ngClass in the templete like.
[ngClass]="{'error': myForm.controls.name.valid == false}"
You have to insert the validator into the form builder object.
Have a quick look at:
https://angular.io/guide/reactive-forms#validatorsrequired
this.heroForm = this.fb.group({
name: ['', [Validators.required] ],
});
As for the button:
<button type="submit" [disabled]="!form.valid" mat-button cdkFocusInitial>Add</button>

Angular material mat-error cannot show message

I have 2 datetime picker, endDate cannot be less than startDate
endDateAfterOrEqualValidator(formGroup): any {
var startDateTimestamp: number;
var endDateTimestamp: number;
startDateTimestamp = Date.parse(formGroup.controls['startDateForm'].value);
endDateTimestamp = Date.parse(formGroup.controls['endDateForm'].value);
return (endDateTimestamp < startDateTimestamp) ? { endDateLessThanStartDate: true } : null;
}
in html:
<mat-form-field>
<input matInput name="endDate" id="endDate" formControlName="endDateForm" [(ngModel)]="endDate" [matDatepicker]="toDatePicker"
placeholder="To Date">
<mat-datepicker-toggle matSuffix [for]="toDatePicker"></mat-datepicker-toggle>
<mat-datepicker disabled="false" #toDatePicker></mat-datepicker>
<mat-error *ngIf="trainingDetail.hasError('endDateLessThanStartDate')">Not valid<mat-error>
</mat-form-field>
with "mat-error", the message does not display. I try to change by "small"
<small *ngIf="trainingDetail.hasError('endDateLessThanStartDate')">Not valid</small>
the message well. Please advice me how to using mat-error
a mat-error only shows when a FormControl is invalid, but you have the validation on your formgroup. So in that case you need to use a ErrorStateMatcher
In your case it would look like this:
export class MyErrorStateMatcher implements ErrorStateMatcher {
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const invalidCtrl = !!(control && control.invalid);
const invalidParent = !!(control && control.parent && control.parent.invalid);
return (invalidCtrl || invalidParent);
}
}
Also worth mentioning, it's not recommended to have two bindings, i.e formControl and ngModel. Remove the ngModel and utilize the form control instead. If you receive your start date and end date at a later point, you can use patchValue (just set some values to form) or setValue (set all values to form)
mark in component the errorstatematcher:
matcher = new MyErrorStateMatcher();
As for your custom validator, you don't need to parse the dates, just check if end date is smaller than start date:
checkDates(group: FormGroup) {
if (group.controls.endDate.value < group.controls.startDate.value) {
return { endDateLessThanStartDate: true }
}
return null;
}
and then mark the error state matcher in your template:
<mat-form-field>
<input matInput [matDatepicker]="picker2" type="text" formControlName="endDate" [errorStateMatcher]="matcher">
<mat-datepicker-toggle matSuffix [for]="picker2"></mat-datepicker-toggle>
<mat-datepicker #picker2></mat-datepicker>
<mat-error *ngIf="myForm.hasError('endDateLessThanStartDate')">End date cannot be earlier than start date</mat-error>
</mat-form-field>
Here's a StackBlitz
If you want to set a control as invalid from the .ts file manually...
HTML:
<mat-form-field class="full-width">
<input matInput [formControl]="exampleFormControl" (change)="changeDetected()">
<mat-hint>(Optional)</mat-hint>
<mat-error *ngIf="exampleFormControl.hasError('invalid')">
Must be a <strong>valid input</strong>!
</mat-error>
</mat-form-field>
TS:
import { FormControl } from '#angular/forms';
#Component({
selector: 'derp',
templateUrl: './derp.html',
styleUrls: ['./derp.scss'],
})
export class ExampleClass {
// Date Error Form Control
exampleFormControl = new FormControl('');
// Variable Check
inputValid: boolean;
changeDetected() {
// Check if input valid
if (this.inputValid) {
console.log('Valid Input');
} else {
console.log('Invalid Input');
// IMPORTANT BIT - This makes the input invalid and resets after a form change is made
this.exampleFormControl.setErrors({
invalid: true,
});
}
}
// CODE THAT CHANGES VALUE OF 'inputValid' //
}

Custom Validator on reactive form for password and confirm password matching getting undefined parameters into Angular 4

I'm trying to implement a custom validator to check if the password and password confirm are equal. The problem is that the validator is getting undefined password and confirmedPassword parameters. How do I make this work. The function works cause if I change the condition to === instead of !== it throws the error correctly when the fields are the same. Does anyone know which is the error here?
signup.component.html
<div class="col-md-7 col-md-offset-1 col-sm-7">
<div class="block">
<div class="well">
<form (onSubmit)="onSubmit()" [formGroup]="signUpForm">
<div class="form-group">
<label for="username" class="control-label">Nombre de usuario:</label>
<input type="text" class="form-control" formControlName="username" title="Please enter your username" placeholder="username">
<p class="help-block" *ngIf="signUpForm.get('username').hasError('required') && signUpForm.get('username').touched">El nombre de usuario es obligatorio</p>
<p class="help-block" *ngIf="signUpForm.get('username').hasError('minlength') && signUpForm.get('username').touched">El nombre de usuario debe tener al menos 6 caracteres</p>
<p class="help-block" *ngIf="signUpForm.get('username').hasError('maxlength') && signUpForm.get('username').touched">El nombre de usuario debe tener menos de 15 caracteres</p>
</div>
<div class="form-group">
<label for="email" class="control-label">E-mail:</label>
<input class="form-control" formControlName="email" title="Please enter your email" placeholder="example#gmail.com">
<p class="help-block" *ngIf="signUpForm.get('email').hasError('required') && signUpForm.get('email').touched">La dirección de email es obligatoria</p>
<p class="help-block" *ngIf="signUpForm.get('email').hasError('email') && signUpForm.get('email').touched">Debe ingresar una dirección de correo válida</p>
</div>
<div class="form-group">
<label for="password" class="control-label">Contraseña:</label>
<input type="password" class="form-control" formControlName="password" title="Please enter your password" [(ngModel)]="password">
<p class="help-block" *ngIf="signUpForm.get('password').hasError('required') && signUpForm.get('password').touched">Debe ingresar una contraseña</p>
</div>
<div class="form-group">
<label for="confirmedPassword" class="control-label">Confirmar Contraseña:</label>
<input type="password" class="form-control" formControlName="confirmedPassword" title="Please re-enter your password" [(ngModel)]="confirmedPassword">
<p class="help-block" *ngIf="signUpForm.get('confirmedPassword').hasError('required') && signUpForm.get('confirmedPassword').touched">La confirmación de contraseña no puede estar vacía</p>
<p class="help-block" *ngIf="signUpForm.get('confirmedPassword').hasError('passwordMismatch') && signUpForm.get('confirmedPassword').touched">Las contraseñas no coinciden</p>
</div>
<button type="submit" class="btn btn-success" [disabled]="!signUpForm.valid">Registrarse</button>
<a routerLink="/signin" class="btn btn-default" style="">Ya tenes usuario? Logueate</a> {{ creationMessage }}
</form>
</div>
</div>
</div>
signup.component.ts
import { Component, OnInit, ViewChild, Input } from '#angular/core';
import { FormControl, FormGroup, Validators } from '#angular/forms';
import { CustomValidators } from '../../shared/custom-validators';
import { Observable } from 'rxjs/Observable';
#Component({
selector: 'app-signup',
templateUrl: './signup.component.html',
styleUrls: ['./signup.component.sass']
})
export class SignupComponent implements OnInit {
signUpForm: FormGroup;
user = {
username: '',
email: '',
password: ''
};
submitted = false;
#Input() password='';
#Input() confirmedPassword='';
constructor() { }
ngOnInit() {
this.signUpForm = new FormGroup({
'username': new FormControl(null, [Validators.required, Validators.minLength(6), Validators.maxLength(15)]),
'email': new FormControl(null, [Validators.required, Validators.email, Validators.minLength(5)]),
'password': new FormControl(null, [Validators.required]),
'confirmedPassword': new FormControl(null, [Validators.required, CustomValidators.passwordsMatch(this.password,this.confirmedPassword).bind(this)])
});
}
onSubmit() {
if (this.signUpForm.valid) {
console.log(this.signUpForm.value);
}
}
}
custom-validators.ts
import { FormControl } from '#angular/forms';
export class CustomValidators{
public static passwordsMatch(password: string, confirmedPassword: string) {
return (control: FormControl) : { [s: string]: boolean } =>{
//getting undefined values for both variables
console.log(password,confirmedPassword);
//if I change this condition to === it throws the error if the
// two fields are the same, so this part works
if (password !== confirmedPassword) {
return { 'passwordMismatch': true }
} else {
//it always gets here no matter what
return null;
}
}
}
}
import {AbstractControl, FormBuilder, FormGroup, Validators} from
set your password input into the group and no need to use "ngModel".
<div class="form-group row" formGroupName="passwords">
<div class="form-group">
<label for="password" class="control-label">Contraseña:</label>
<input type="password" class="form-control" formControlName="password" title="Please enter your password">
<p class="help-block" *ngIf="signUpForm.get('password').hasError('required') && signUpForm.get('password').touched">Debe ingresar una contraseña</p>
</div>
<div class="form-group">
<label for="confirmedPassword" class="control-label">Confirmar Contraseña:</label>
<input type="password" class="form-control" formControlName="confirmedPassword" title="Please re-enter your password">
<p class="help-block" *ngIf="signUpForm.get('confirmedPassword').hasError('required') && signUpForm.get('confirmedPassword').touched">Password must be required</p>
<p class="help-block" *ngIf="signUpForm.get('confirmedPassword').hasError('passwordMismatch') && signUpForm.get('confirmedPassword').touched">password does not match</p>
</div>
buildForm(): void {
this.userForm = this.formBuilder.group({
passwords: this.formBuilder.group({
password: ['', [Validators.required]],
confirm_password: ['', [Validators.required]],
}, {validator: this.passwordConfirming}),
});
}
add this custom function for validate password and confirm password
passwordConfirming(c: AbstractControl): { invalid: boolean } {
if (c.get('password').value !== c.get('confirm_password').value) {
return {invalid: true};
}
}
Display error when password does not match
<div style='color:#ff7355' *ngIf="userForm.get(['passwords','password']).value != userForm.get(['passwords','confirm_password']).value && userForm.get(['passwords','confirm_password']).value != null">
Password does not match</div>
The issue is that you are mixing the reactive forms module with the input approach. This is causing you to get undefined when passing the values to the validator.
You don't need to bind to the ng-model when using the reactive forms. Instead, you should access the value of the fields from the Instance of FormGroup.
I do something like this in an app to validate the passwords match.
public Credentials: FormGroup;
ngOnInit() {
this.Credentials = new FormGroup({});
this.Credentials.addControl('Password', new FormControl('', [Validators.required]));
this.Credentials.addControl('Confirmation', new FormControl(
'', [Validators.compose(
[Validators.required, this.validateAreEqual.bind(this)]
)]
));
}
private validateAreEqual(fieldControl: FormControl) {
return fieldControl.value === this.Credentials.get("Password").value ? null : {
NotEqual: true
};
}
Note that the validator expects a FormControl field as a parameter and it compares the value of the field to that of the value of the Password field of the Credentials FormGroup.
In the HTML make sure to remove the ng-model.
<input type="password" class="form-control" formControlName="confirmedPassword" title="Please re-enter your password" >
<!-- AND -->
<input type="password" class="form-control" formControlName="password" title="Please enter your password">
Hope this helps!
There are two types of validators: FormGroup validator and FormControl validator. To verify two passwords match, you have to add a FormGroup validator. Below is my example:
Note: this.fb is the injected FormBuilder
this.newAccountForm = this.fb.group(
{
newPassword: ['', [Validators.required, Validators.minLength(6)]],
repeatNewPassword: ['', [Validators.required, Validators.minLength(6)]],
},
{validator: this.passwordMatchValidator}
);
passwordMatchValidator(frm: FormGroup) {
return frm.controls['newPassword'].value === frm.controls['repeatNewPassword'].value ? null : {'mismatch': true};
}
and in the templeate:
<div class="invalid-feedback" *ngIf="newAccountForm.errors?.mismatch && (newAccountForm.controls['repeatNewPassword'].dirty || newAccountForm.controls['repeatNewPassword'].touched)">
Passwords don't match.
</div>
The key point here is to add the FormGroup validator as the second parameter to the group method.
Please update FormGroup code like below in Angular5
this.signUpForm = new FormGroup({
'username': new FormControl(null, [Validators.required, Validators.minLength(6), Validators.maxLength(15)]),
'email': new FormControl(null, [Validators.required, Validators.email, Validators.minLength(5)]),
'password': new FormControl(null, [Validators.required]),
'confirmedPassword': new FormControl(null, [Validators.required])
}, this.pwdMatchValidator);
Add pwdMatchValidator function in your component
pwdMatchValidator(frm: FormGroup) {
return frm.get('password').value === frm.get('confirmedPassword').value
? null : {'mismatch': true};
}
Add validation message in your template
<span *ngIf="confirmedPassword.errors || signUpForm .errors?.mismatch">
Password doesn't match
</span>
Please find the below angular material working component.
Component Templete Code password.component.html
<form class="cahnge-pwd-form" (ngSubmit)="onSubmit()" name="passwordForm" [formGroup]="passwordForm" #formDir="ngForm">
<div fxLayout='column'>
<mat-form-field>
<input matInput name="password" placeholder="Password" [type]="hide ? 'text' : 'password'" formControlName="password" required>
<mat-icon matSuffix (click)="hide = !hide">{{hide ? 'visibility_off' : 'visibility'}}</mat-icon>
<mat-error *ngIf="password.invalid && (password.dirty || password.touched || isSubmit)">
<span *ngIf="password.errors.required">
Please enter a Password.
</span>
<span *ngIf="password.errors.maxlength">
Please enter a Email no more than 16 characters.
</span>
<span *ngIf="password.errors.minlength">
Please enter a password at least 6 characters.
</span>
</mat-error>
</mat-form-field>
<mat-form-field>
<input matInput name="password" placeholder="Confirm Password" [type]="confirm_hide ? 'text' : 'password'" formControlName="confirm_password"
required>
<mat-icon matSuffix (click)="confirm_hide = !confirm_hide">{{confirm_hide ? 'visibility_off' : 'visibility'}}</mat-icon>
<mat-error *ngIf="(confirm_password.invalid && (confirm_password.dirty || confirm_password.touched || isSubmit) || passwordForm.errors?.mismatch)">
<span *ngIf="confirm_password.errors || passwordForm.errors?.mismatch">
Password doesn't match
</span>
</mat-error>
</mat-form-field>
<div fxLayout='row' fxLayoutGap="10px">
<button type="submit" mat-raised-button color="primary">Submit</button>
<button type="button" (click)="formDir.resetForm(passwordForm)" mat-raised-button color="warn">Cancel</button>
</div>
</div>
</form>
Component Code : password.component.ts
import { Component, OnInit, AfterViewInit } from '#angular/core';
import { FormControl, FormGroup, Validators, FormBuilder } from '#angular/forms';
import { ToastrService } from 'ngx-toastr';
import { Router, ActivatedRoute, ParamMap } from '#angular/router';
import { PasswordService } from './password.service';
import { PasswordValidation } from './confirm';
#Component({
selector: 'app-password',
templateUrl: './password.component.html',
styleUrls: ['./password.component.css']
})
export class PasswordComponent implements OnInit {
passwordForm: FormGroup;
isSubmit: boolean;
constructor(private router: Router, private passwordService: PasswordService, private toastrService: ToastrService, private route: ActivatedRoute) { }
ngOnInit() {
this.passwordForm = new FormGroup({
'password': new FormControl('', [
Validators.required,
Validators.minLength(6),
Validators.maxLength(16),
]),
'confirm_password': new FormControl('', [
Validators.required,
Validators.minLength(6),
Validators.maxLength(16),
]),
}, this.pwdMatchValidator);
}
pwdMatchValidator(frm: FormGroup) {
return frm.get('password').value === frm.get('confirm_password').value
? null : {'mismatch': true};
}
get password() { return this.passwordForm.get('password'); }
get confirm_password() { return this.passwordForm.get('confirm_password'); }
onSubmit(formvalue):boolean {
this.isSubmit = true;
if (this.passwordForm.invalid) {
return false;
} else {
this.passwordService.updatePassword(this.passwordForm.value)
.subscribe((res) => {
if (res.status == 'success') {
this.toastrService.success(res.msg);
this.router.navigate(['/change-password']);
}
})
return true;
}
}
}
I would do the same as Shailesh Ladumor but adding the following line before returning in the validation function:
c.get('confirm_password').setErrors({'noMatch': true});
So that the validation function looks like this:
passwordConfirming(c: AbstractControl): { invalid: boolean } {
if (c.get('password').value !== c.get('confirm_password').value) {
c.get('confirm_password').setErrors({'noMatch': true});
return {invalid: true};
}
}
This will not only set the hole userForm as an invalid form group, but it will also set confirm_password as an invalid form control.
With this you can later call the following function in your template:
public getPasswordConfirmationErrorMessage() {
if (this.userForm.get('confirm_password').hasError('required')) {
return 'You must retype your password';
} else if (this.userForm.get('confirm_password').hasError('noMatch')) {
return 'Passwords do not match';
} else {
return '';
}
}
When you need to validate on more than one field, and you wish to declare validator at form creation time, FormGroup validator must be used. The main issue with form validator is that it attaches error to form and not to validating control, which leeds to some inconsistents in template. Here is reusable form validator wich attaches error to both form and control
// in validators.ts file
export function controlsEqual(
controlName: string,
equalToName: string,
errorKey: string = controlName // here you can customize validation error key
) {
return (form: FormGroup) => {
const control = form.get(controlName);
if (control.value !== form.get(equalToName).value) {
control.setErrors({ [errorKey]: true });
return {
[errorKey]: true
}
} else {
control.setErrors(null);
return null
}
}
}
// then you can use it like
ngOnInit() {
this.vmForm = this.fb.group({
username: ['', [Validators.required, Validators.email]],
password: ['', [
Validators.required,
Validators.pattern('[\\w\\d]+'),
Validators.minLength(8)]],
confirm: [''], // no need for any validators here
}, {
// here we attach our form validator
validators: controlsEqual('confirm', 'password')
});
}
this.myForm = this.fb.group({
userEmail: [null, [Validators.required, Validators.email]],
pwd: [null, [Validators.required, Validators.minLength(8)]],
pwdConfirm: [null, [Validators.required]],
}, {validator: this.pwdConfirming('pwd', 'pwdConfirm')});
pwdConfirming(key: string, confirmationKey: string) {
return (group: FormGroup) => {
const input = group.controls[key];
const confirmationInput = group.controls[confirmationKey];
return confirmationInput.setErrors(
input.value !== confirmationInput.value ? {notEquivalent: true} : null
);
};
}
Just want to add. I use Reactive form, so it is easy to use subscription for valueChanges on the form for custom validation
this.registrationForm.valueChanges.subscribe(frm => {
const password = frm.password;
const confirm = frm.confirmPassword;
if (password !== confirm) {
this.registrationForm.get('confirmPassword').setErrors({ notMatched: true });
}
else {
this.registrationForm.get('confirmPassword').setErrors(null);
}
});
}
When you're creating the validator, you're passing in the values of password and confirmedPassword, but changes in those values will not be reflected in the validator.
The two options I see are:
define your validator on the FormGroup, and look up the values from the two controls you want to compare; or
since you're binding to this already, use this.password and this.confirmedPassword in your validator.
Just for the variety, I'm adding one more way to do this. Basically, I created a simple custom validator that takes the original control (first entered field) and checks it's value with the re-entry control (second control).
import { AbstractControl, ValidatorFn } from "#angular/forms";
export abstract class ReEnterValidation {
static reEnterValidation(originalControl: AbstractControl): ValidatorFn {
return (control: AbstractControl): { [s: string]: boolean } => {
if (control.value != originalControl.value) {
return { 'reentry': true };
}
};
}
}
You can either set this validator initially when creating a new form control:
control1 = new FormControl('', ReEnterValidation.reEnterValidation(control2));
Or, you can set it afterwards:
control.setValidators(ReEnterValidation.reEnterValidation(this._newPinControl));
control.updateValueAndValidity();
In the view, you can show the errors as following:
<!-- Confirm pin validation -->
<div *ngIf="control2.invalid && (control2.dirty || control2.touched)">
<div *ngIf="control2.errors.reentry">
The re-entered password doesn't match. Please try again.
</div>
</div>
import { AbstractControl, FormControl, FormGroup, Validators } from '#angular/forms';
public newForm: FormGroup | any;
ngOnInit(): void {
this.newForm = new FormGroup({
password: new FormControl(null, [Validators.required]),
confirm: new FormControl(null, [Validators.required])
}, {validators: this.validateAreEqual})
}
public validateAreEqual(c: AbstractControl): {notSame: boolean} {
return c.value.password === c.value.confirm ? {notSame: false} : {notSame: true};
}
<span class="error-text" *ngIf="newForm.get('confirm').touched && newForm.errors['notSame']">Password mismatch </span>

Categories

Resources