I'm trying to create a custom checkbox component to replace an old-old NG Prime version that had been being used but wanted to make it leaner/cleaner with some added aria points and utilizing more of the checkbox attributes directly. The problem I'm running into though (I'm guessing) is with updating the value accessor / ngmodel and could use some guidance on what I'm apparently missing...
I thought what I had was simplistic enough but apparently not. The idea is that @Input() binary
is set to true
by default expecting ngModel
to have a boolean to use but I don't get the value from ngModel
when it's initialized. I need ngModel
to communicate that to the component though when it's used standalone as like <app-checkbox [ngModel]="blah"></app-checkbox>
but also have value_accessor
working as expected if it's used in say a reactive angular form etc.
I also get a "NG0303: Can't bind to 'checked' since it isn't a known property of 'app-checkbox'" in my instance, but it's not on the stackblitz which I don't understand.
👉 A Stackblitz showing the implementation / problem
(the example is angular 12, I use 13, but it's fine for the example)
And for quick code reference;
.ts
import {
Component,
OnInit,
Input,
Output,
EventEmitter,
forwardRef,
ViewChild,
ElementRef,
ChangeDetectorRef,
ChangeDetectionStrategy,
} from '@angular/core';
import {
NG_VALUE_ACCESSOR,
ControlValueAccessor,
FormControl,
} from '@angular/forms';
import { ObjectUtils } from '../objectUtils';
export const CHECKBOX_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CheckboxComponent),
multi: true,
};
@Component({
selector: 'app-checkbox',
templateUrl: './checkbox.component.html',
styleUrls: ['./checkbox.component.css'],
providers: [CHECKBOX_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CheckboxComponent implements OnInit, ControlValueAccessor {
@Input() value: any;
@Input() name: string;
@Input() disabled: boolean;
@Input() label: string;
@Input() ariaLabelledBy: string;
@Input() ariaLabel: string;
@Input() tabindex: number;
@Input() id: string;
@Input() labelStyleClass: string;
@Input() formControl: FormControl;
@Input() required: boolean = false;
@Input() isValid: boolean = true;
@Input() invalidMessage: string;
// This is set true by default now since every instance at the time this replacement component was made requires it for a boolean value bound to ngModel
// If in the future we start using angular reactive forms more it can be toggled at the instance.
@Input() binary: boolean = true;
@ViewChild('cb') inputViewChild: ElementRef;
@Output() onChange: EventEmitter<any> = new EventEmitter();
isChecked: boolean = false;
focused: boolean = false;
model: any;
onModelChange: Function = () => {};
onModelTouched: Function = () => {};
constructor(private cd: ChangeDetectorRef) {}
ngOnInit(): void {
this.id = this.id
? this.id
: `tcl-cb-${Math.random()
.toString()
.substr(2, length ? length : 6)}`;
}
updateModel(event) {
let newModelValue;
if (!this.binary) {
if (this.isChecked) {
newModelValue = this.model.filter(
(val) => !ObjectUtils.equals(val, this.value)
);
} else {
newModelValue = this.model ? [...this.model, this.value] : [this.value];
}
this.onModelChange(newModelValue);
this.model = newModelValue;
if (this.formControl) {
this.formControl.setValue(newModelValue);
}
} else {
newModelValue = this.isChecked;
this.model = newModelValue;
this.onModelChange(newModelValue);
}
this.onChange.emit({ checked: newModelValue, originalEvent: event });
}
handleChange(event) {
this.isChecked = event.srcElement.checked;
if (!this.disabled) {
this.updateModel(event);
}
}
onFocus() {
this.focused = true;
}
onBlur() {
this.focused = false;
this.onModelTouched();
}
focus() {
this.inputViewChild.nativeElement.focus();
}
writeValue(model: any): void {
this.model = model;
this.cd.markForCheck();
}
registerOnChange(fn: Function): void {
this.onModelChange = fn;
}
registerOnTouched(fn: Function): void {
this.onModelTouched = fn;
}
setDisabledState(val: boolean): void {
this.disabled = val;
this.cd.markForCheck();
}
}
html
<div role="group" class="app-checkbox">
<input
#cb
type="checkbox"
[attr.id]="id"
[attr.name]="name"
[disabled]="disabled"
[value]="value"
[checked]="isChecked"
[attr.tabindex]="tabindex"
[attr.required]="required"
[attr.aria-labelledby]="ariaLabelledBy"
[attr.aria-label]="ariaLabel"
[attr.aria-checked]="isChecked"
(focus)="onFocus()"
(blur)="onBlur()"
(change)="handleChange($event)"
/>
<label *ngIf="label" [attr.for]="id" [class]="labelStyleClass">
{{ label }} <sup *ngIf="required">*</sup>
</label>
<div *ngIf="!isValid" class="tcl-checkbox-invalid-msg">
{{ invalidMessage }}
</div>
</div>
Any insight is appreciated especially if there's anything additional being forgotten!
I need ngModel to communicate that to the component though when it's used standalone as like <app-checkbox [ngModel]="blah"> but also have value_accessor working as expected if it's used in say a reactive angular form etc.
In your case ngModel
does communicate the value to your custom checkbox component, but you are not using the received value within your template to bind it to the input
.
When using
<app-checkbox [ngModel]="isChecked"></app-checkbox>
or
<!-- If isChecked is FormControl name -->
<app-checkbox formControlName="isChecked"></app-checkbox>
the writeValue
method is called passing it the form control value.
Within writeValue
method you are setting this.model
, but in your template you are using isChecked
to bind to input checked
property, and hence the checkbox isn't checked. You need to bind the value received from forms module to the correct property within your custom component, in your case it would be:
// within writeValue method
this.isChecked = model;
I also get a "NG0303: Can't bind to 'checked' since it isn't a known property of 'app-checkbox'" in my instance, but it's not on the stackblitz which I don't understand.
If you are getting the above error when doing something as below, then that's a valid error which you will also get in your stackblitz example. The reason being, CheckboxComponent
doesn't have any input property named checked
.
<app-checkbox [checked]="isChecked"></app-checkbox>