import { Component, forwardRef, Input, Output, EventEmitter, ChangeDetectorRef, ViewChild, OnInit, inject, } from "@angular/core"; import { FormControl, ControlValueAccessor, NG_VALUE_ACCESSOR, } from "@angular/forms"; import { FieldsetWrapper } from "../../wrappers/fieldset-wrapper"; import { OptionsType } from "../../types"; import { SamFormService } from "../../form-service"; /** * The component is a set of checkboxes */ @Component({ selector: "sam-checkbox", templateUrl: "checkbox.template.html", providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SamCheckboxComponent), multi: true, }, ], standalone: false, }) export class SamCheckboxComponent implements ControlValueAccessor, OnInit { protected samFormService = inject(SamFormService); private cdr = inject(ChangeDetectorRef); /** * Deprecated, Sets the bound value of the component */ @Input() model: (string | number)[] = []; /** * Sets the array of checkbox values and labels (see OptionsType[]) */ @Input() options: OptionsType[]; /** * Sets the label text */ @Input() label: string; /** * Sets the semantic description for the component */ @Input() name: string; /** * Sets helpful text for the using the component */ @Input() hint: string; /** * Sets required text on component */ @Input() required: boolean = false; /** * Sets the form control error message */ @Input() errorMessage: string; /** * If true, an addition checkbox is added that selects all the checkboxes */ @Input() hasSelectAll: boolean; /** * Sets the angular FormControl */ @Input() control: FormControl; /** * Toggles validations to display with SamFormService events */ @Input() useFormService: boolean; /** * Sets disabled state */ @Input() disabled: boolean; /** * Sets the id */ @Input() id: string; public optionChange: string | number; public optionId: string; /** * Deprecated, Event emitted when the model value changes */ @Output() modelChange: EventEmitter<(string | number)[]> = new EventEmitter< (string | number)[] >(); @Output() optionSelected: EventEmitter<{ model: (string | number)[]; selected: string | number; id: string; }> = new EventEmitter(); @ViewChild(FieldsetWrapper, { static: true }) public wrapper: FieldsetWrapper; activeOptions = 0; /* * We want our model to list the checked items in the order that they appear * in the options list. This object allows us to efficiently determine if a * value is before another value */ private _ordering: Record = {}; onChange: (value: (string | number)[]) => void = () => undefined; onTouched: () => void = () => undefined; get value() { return this.model; } set value(val: (string | number)[]) { this.setModelValue(val); this.onChange(this.model); this.onTouched(); } ngOnInit() { // initialize the order lookup map for (let i = 0; i < this.options.length; i++) { const val = this.options[i].value; this._ordering[val] = i; } if (this.control) { this.control.valueChanges.subscribe(() => { this.wrapper.formatErrors(this.control); }); this.wrapper.formatErrors(this.control); } this.setSelectAllCheck(); } setModelValue(val: (string | number)[]) { let returnVal = val; if (!Array.isArray(returnVal)) { returnVal = []; } // don't select options that are disabled for (const idx in this.options) { const lookup = returnVal.findIndex((value) => { return value === this.options[idx].value; }); if (this.options[idx].disabled && lookup !== -1) { returnVal.splice(lookup, 1); } } this.model = returnVal; this.cdr.detectChanges(); this.setSelectAllCheck(); } setSelectAllCheck() { let activeOptionsNum = 0; this.options.forEach((val) => { if (!val.disabled) { activeOptionsNum++; } }); this.activeOptions = activeOptionsNum; } // Give the check all label a name for screen readers checkAllLabelOrId() { let labelOrId; if (this.id) { labelOrId = `all-${this.id}`; } else { labelOrId = `all-${this.label}`; } return labelOrId; } isChecked(value: string | number) { return this.model.indexOf(value) !== -1; } onCheckChanged(value: string | number, isChecked: boolean, id: string) { this.onTouched(); this.optionChange = value; this.optionId = id; if (!isChecked) { // If the option was unchecked, remove it from the model this.value = this.model.filter((val) => val !== value); } else { // Else, insert the checked item into the model in the correct order let i = 0; const thisOrder = this._ordering[value]; while (i < this.model.length) { const otherValue = this.model[i]; // If the item being inserted is after the current value, break and // insert it. if (thisOrder <= this._ordering[otherValue]) { break; } i++; } const clone = this.model.indexOf("") > -1 ? this.model.slice(1) : this.model.slice(0); clone.splice(i, 0, value); this.value = clone; } this.emitModel(); } onSelectAllChange(isSelectAllChecked: boolean) { this.onTouched(); this.value = !isSelectAllChecked ? [] : this.options.map((option) => option.value); this.emitModel(); } emitModel() { this.modelChange.emit(this.model); this.optionSelected.emit({ model: this.model, selected: this.optionChange, id: this.optionId, }); } registerOnChange(fn: (value: (string | number)[]) => void) { this.onChange = fn; } registerOnTouched(fn: () => void) { this.onTouched = fn; } setDisabledState(disabled: boolean) { this.disabled = disabled; } writeValue(value: (string | number)[]) { let returnValue = value; if (!returnValue) { returnValue = []; } this.setModelValue(returnValue); } }