mercredi 2 août 2023

Angular: Custom checkbox doesn't reflect model but standard checkbox does

I am struggling with my custom checkbox not reflecting changes of ngModel. I've probably read all relevant issues on SO and still can't find a solution that works for me.

export class CheckboxInputComponent extends AbstractInputComponent implements ControlValueAccessor, Validator  {

@Input() public inxs: string;
    @Input() public grouped = true;
    @Input() public checked: boolean;
    @Input() public labeled = true;

    @Input() public name: string;

    @Input() public intermediate = false;

    @ViewChild('cb') public cb: ElementRef;

    @ContentChild('checkBoxLabel') labelTemplate;

    constructor ( protected cdr: ChangeDetectorRef ) {
        super(cdr);
    }

    public toggleCheck(): void {
        if (!this.grouped) {
            this.value = !this.value;
            this.checked = !this.checked;
        }
        this.intermediate = false;
    }

    private checkIntermediate(c: FormControl): void {
        if ( this.cb ) {
            if ( !(c.dirty || c.touched) ) {
                this.cb.nativeElement.intermediate = this.intermediate;
            } else {
                this.cb.nativeElement.intermediate = false;
            }
        }
    }

    protected customValidate( c: FormControl ): unknown | null {
        this.checkIntermediate(c);
        if ( c.dirty || c.touched ) {
            const noValue = this.hasNoValue(c);
            if ( this.required && noValue ) {
                return { 'required': true };
            }
        }
        return null;
    }

    protected normalize ( v: any ): any {
        return v;
    }

    protected hasNoValue(c: FormControl ): boolean {
        return c.value !== true;
    }
}

The abstract class it extends is:

@Directive()
export abstract class AbstractInputComponent implements OnChanges {

    /**
     * inner value
     */
    protected innerValue: unknown = null;
    protected innerErrors: AbstractInputErrors;

    /**
     * field label
     */
    @Input() public label: string;

    /**
     * required boolean
     */
    @Input() public required = false;


    /**
     * read only
     */
    @Input() public isReadOnly = false;

    /**
     * placeholder to show
     */
    @Input() public placeholder: string;

    @Input() public disabled = false;

    /**
     * control reference
     */
    @ViewChild('fieldInput') public fieldInput: NgModel;

    public isFocused = false;

    // infrastructure
    public registerOnChange(fn: any) { this.propagateChange = fn; }
    public registerOnTouched(fn: any) { this.propagateTouch = fn; }

    public propagateChange = (_: any) => { };
    public propagateTouch = (_: any) => { };

    constructor(
        protected cdr: ChangeDetectorRef
        ) { }

    public ngOnChanges(): void {
        if ( this.isReadOnly ) {
            this.propagateChange(this.value);
        }
        this.cdr.detectChanges();
    }

    public onFocus(): void {
        this.isFocused = true;
    }

    public onBlur(): void {
        this.isFocused = false;
        this.propagateTouch(true);
    }

    public get value(): any {
        return this.innerValue;
    };

    @Input() public set value(value: any) {
        if ( value !== undefined ) {
            this.writeValue(value);
            this.propagateChange(this.innerValue);
            this.propagateTouch(true);
        }
    }

    public writeValue(value: any): void {
        if (value !== this.innerValue) {
            this.innerValue = this.normalize(value);
        }
    }

    public setDisabledState(state: boolean): void {
        this.disabled = state;
    }

    public get errors(): AbstractInputErrors {
        return this.innerErrors;
    }

    public set errors( err: AbstractInputErrors ) {
        if ( this.fieldInput ) {
            if ( err ) {
                this.innerErrors = err;
            }
            this.fieldInput.control.setErrors(err);
        }
    }

    public validate(c: FormControl): unknown | null {
        this.innerErrors = (this.isReadOnly) ? null : this.customValidate(c);
        this.errors = this.innerErrors;
        return this.errors;
    }

    protected abstract normalize(value)
    protected abstract customValidate(control)
    protected abstract hasNoValue(value)
}

And the template:

<div class="ultim-checkbox"
    [class.readonly]="isReadOnly"
    [class.checked]="value"
    [class.intermediate]="intermediate"
    (click)="toggleCheck()">
    <input type="checkbox"
        [required]="required"
        [checked]="checked"
        [(ngModel)]="value"
        [attr.intermediate]="intermediate || null"
        #fieldInput="ngModel"
        #cb>
    <span><i class="ico"
            [ngClass]="{
                'i-check-fat': !intermediate,
                'i-minus-fat': intermediate
            }"></i></span>
</div>

Now, in the parent component it goes like this:

                <ultim-checkbox *ngIf="multiEditMode"
                    [(ngModel)]="data.meta.isSelected"
                    (change)="onSelect(data)"></ultim-checkbox>

It only updates the checkbox state if I hover over it. If instead I swap it for the standard input type="checkbox" it works just fine without hovering. Can you spot a problem?




Aucun commentaire:

Enregistrer un commentaire