Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular 2 / Material - md-error not displayed with custom validator applied to parent FormGroup

I am having some trouble getting validation errors to display on a model driven form with Angular (v4.3.6).

In my model, I have the following:

this.registerForm = formBuilder.group({
        'email':[null,Validators.compose([Validators.required, ValidateEmail])],
        'firstName':[null, Validators.required],
        'lastName':[null, Validators.required],
        'passwordGroup': formBuilder.group({
            'password':[null, Validators.compose([Validators.required,Validators.minLength(8)])],
            'passwordConfirmation':[null, Validators.required],
        },{validator: ValidatePasswordConfirmation})
    });

The ValidatePasswordConfirmation custom validator referenced is as follows:

export function ValidatePasswordConfirmation(group: FormGroup) {

    if(group.value.password !== group.value.passwordConfirmation){
        return { 'no-match':true };
    }

    return null;
}

Lastly, in my template I have the following:

<md-form-field>
  <input mdInput name="passwordConfirmation" placeholder="Confirm password" [formControl]="registerForm.controls['passwordGroup'].controls['passwordConfirmation']" [(ngModel)]="model.passwordConfirmation" type="password">

<md-error *ngIf="registerForm.controls['passwordGroup'].controls['passwordConfirmation'].hasError('required')">
    Password confirmation is required
  </md-error>

<md-error *ngIf="registerForm.controls['passwordGroup'].hasError('no-match')">
    Passwords don't match
  </md-error>

</md-form-field>

However, the md-error governed by the 'no-match' error never shows up. So, to debug this on the page I added the following:

no-match = {{registerForm.controls['passwordGroup'].hasError('no-match')}} 
invalid = {{registerForm.controls['passwordGroup'].invalid}}

Unsurprisingly, the debug lines show true/false as you would expect. However, the 'md-error' is never displayed... except for when the 'required' error is shown. I have a feeling that the issue is due to the [formControl] referring to the passwordConfirmation FormControl, and so without that being invalid, the md-error is not shown. However, I would like this error to display when the outer FormGroup is invalid.

Any pointers on where I'm going wrong here would be really helpful!

Finally, I have tried a few other ways around this such as setting the error on the PasswordConfirmation FormControl, which works, but I would like to know why my current implementation is failing.

like image 786
user1615376 Avatar asked Sep 20 '17 19:09

user1615376


1 Answers

I found that I was able to solve the exact same issue using a custom ErrorStateMatcher (the default one requires the control to be in an invalid state before showing any errors)

export class ParentErrorStateMatcher implements ErrorStateMatcher {
    isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
        const isSubmitted = !!(form && form.submitted);
        const controlTouched = !!(control && (control.dirty || control.touched));
        const controlInvalid = !!(control && control.invalid);
        const parentInvalid = !!(control && control.parent && control.parent.invalid && (control.parent.dirty || control.parent.touched));

        return isSubmitted || (controlTouched && (controlInvalid || parentInvalid));
    }
}

This is exposed as a variable on my page component like so...

@Component({
    templateUrl: 'register.page.component.html',
    styleUrls: ['register.page.styles.css']
})
export class RegisterPageComponent implements OnInit {
    registerForm: FormGroup;
    parentErrorStateMatcher = new ParentErrorStateMatcher();

    // Accessors
    get name() { return this.registerForm.get('name'); }
    get email() { return this.registerForm.get('email'); }
    get passwords() { return this.registerForm.get('passwords'); }
    get password() { return this.registerForm.get('passwords.password'); }
    get confirmPassword() { return this.registerForm.get('passwords.confirmPassword'); }
...

With this form...

this.registerForm = this.formBuilder.group({
        name: ['', [
            Validators.required,
            Validators.maxLength(256)]
        ],
        email: ['', [
            Validators.email,
            Validators.required,
            Validators.maxLength(256)]
        ],
        passwords: this.formBuilder.group({
            password: ['', [
                Validators.required,
                Validators.maxLength(128)
            ]],
            confirmPassword: ['', [
                Validators.required
            ]]
        },
            {
                validator: CustomValidators.doNotMatch('password', 'confirmPassword')
            }),
    });

And this mark-up for the password fields (see errorStateMatcher and the last mat-error for the confirmPassword input control)...

<div formGroupName="passwords">
    <mat-form-field class="full-width">
        <input matInput placeholder="Password" type="password" name="password" id="password" formControlName="password" required/>
        <mat-error *ngIf="password.errors && password.errors.required">
            Please enter your password
        </mat-error>
        <mat-error *ngIf="password.errors && password.errors.maxLength">
            Password must be less than 128 characters long
        </mat-error>
    </mat-form-field>

    <mat-form-field class="full-width">
        <input matInput placeholder="Confirm Password" type="password" name="confirmPassword" id="confirmPassword" formControlName="confirmPassword" required
            [errorStateMatcher]="parentErrorStateMatcher"/>
        <mat-error *ngIf="confirmPassword.errors && confirmPassword.errors.required">
            Please confirm your password
        </mat-error>
        <mat-error *ngIf="passwords.errors && passwords.errors.doNotMatch">
            Passwords do not match
        </mat-error>
    </mat-form-field>
</div>

Feels more marginally more confusing than when I wasn't using Material but I'm happy for the trade-off and really the only extra code is the custom matcher :)

like image 105
George Goodchild Avatar answered Nov 03 '22 00:11

George Goodchild