1
votes

I've created a custom mat-checkbox component and implemented the ControlValueAccessor interface in order to communicate with the passed formControl or ngModel.

The checkbox updates the assigned formControl on change, however my problem is that it doesn't seem to register validator functions either template-driven or the reactive way.

This is my component:

import {Component, Input, OnInit, Self, ViewEncapsulation} from '@angular/core';
import {ControlValueAccessor, NgControl} from '@angular/forms';

@Component({
    selector: 'app-checkbox',
    templateUrl: './checkbox.component.html',
    styleUrls: ['./checkbox.component.scss'],
    encapsulation: ViewEncapsulation.None
})
export class CheckboxComponent implements OnInit, ControlValueAccessor {

    @Input() id: string;
    @Input('aria-label') ariaLabel: string;
    @Input() label: string;

    private isChecked = false;
    private isDisabled = false;

    onChange = (isChecked: boolean) => {};
    onTouched = () => {};

    constructor(@Self() public controlDir: NgControl) {
        // providing component as ControlValueAccessor
        controlDir.valueAccessor = this;
    }

    ngOnInit() {
        // Initializing assigned FormControl and providing it's Validator
        const control = this.controlDir.control;
        control.setValidators(control.validator);
        control.updateValueAndValidity();
    }

    /**
     * ControlValueAccessor interface (https://angular.io/api/forms/ControlValueAccessor)
     * Registers a callback function that is called when the control's value changes in the UI.
     * @param fn callback function
     */
    registerOnChange(fn: (value: boolean) => void): void {
        this.onChange = fn;
    }

    /**
     * Input change callback that passes the changed string to this.onChange method
     */
    valueChanged(value: boolean) {
        this.onChange(value);
    }

    /**
     * ControlValueAccessor interface
     * Registers a callback function is called by the forms API on initialization to update the form model on blur.
     * @param fn callback function
     */
    registerOnTouched(fn: () => void): void {
        this.onTouched = fn;
    }

    /**
     * (optional) - ControlValueAccessor interface
     * Function that is called by the forms API when the control status changes to or from 'DISABLED'. Depending on the status, it enables or disables the appropriate DOM element.
     * @param isDisabled control state
     */
    setDisabledState(isDisabled: boolean): void {
        this.isDisabled = isDisabled;
    }

    /**
     * ControlValueAccessor interface
     * Writes a new value to the element.
     * @param value new control value
     */
    writeValue(value: boolean): void {
        if (value) {
            this.isChecked = value;
        }
    }
}

And this my template:

<mat-checkbox color="primary" [checked]="isChecked" [disabled]="isDisabled" [id]="id" [attr.aria-label]="ariaLabel" (change)="valueChanged($event.checked)" (blur)="onTouched()">
    {{label}}
</mat-checkbox>

This is how I call my component the template-driven way:

<form class="row border-bottom" #checkboxForm2="ngForm">
    <div class="col-12 col-md-4 my-1">
        checkboxForm2.value: <br>
        {{checkboxForm2.value | json}} <br>
        checkboxForm2.valid: {{checkboxForm2.valid}}
    </div>
    <div class="col-12 col-md-8 my-3">
        <app-checkbox [ngModel]="false" name="checkbox3" label="REQUIRED" required></app-checkbox>
    </div>
</form>

checkboxForm2.valid always stays true. How can I make the required validator work?

1
The code looks fine, can you try console.log(control.validator) and make sure required is there?sabithpocker
Why is onChange() doing nothing in your custom control?sabithpocker
Also in your case you may even have to go for requiredTrue validator, not sure if required works when you have the value as falsesabithpocker
Thanks sabithpocker, the requiredTrue validator worked when I set it on the formControl. Is there also directive I can set with ngModel?Boldizsar
onChange() was originally set on the change event of my checkbox. I just played around with the valueChanged function.Boldizsar

1 Answers

0
votes

I managed to make it work by calling a function checking whether if the control has Validators.required attached and setting Validators.requiredTrue in the writeValue CVA interface method of the component. Here's my code:

export class CheckboxComponent implements DoCheck, ControlValueAccessor {

...

    writeValue(value: boolean): void {
        this._isChecked = !!value;
        this._autoSelected = !!value;

        const control = this._controlDir.control;
        // Adding Validators.requiredTrue if Validators.required is set on control in order to make it work with template-driven required directive
        if (this.hasRequiredValidator(control)) {
            control.setValidators([control.validator, Validators.requiredTrue]);
        } else {
            control.setValidators(control.validator);
        }
    }

...

    /**
     * Returns whether checkbox is required
     * @param abstractControl control assigned to component
     */
    hasRequiredValidator(abstractControl: AbstractControl): boolean {
        if (abstractControl.validator) {
            const validator = abstractControl.validator({} as AbstractControl);
            if (validator && validator.required) {
                return true;
            }
        }
        return false;
    }