import {
booleanAttribute,
ChangeDetectionStrategy,
Component,
computed,
forwardRef,
inject,
input,
model,
signal,
} from '@angular/core';
import {
type ControlValueAccessor,
NG_VALUE_ACCESSOR,
} from '@angular/forms';
import { AfricaniesIconComponent } from '@africanies/africanies-icons';
import { ModeColorService } from '@africanies/africanies-theme';
import {
FORM_ERROR_CLASS,
FORM_FOCUS_VISIBLE_PEER_CLASS,
FORM_HINT_CLASS,
} from '../form-field.classes';
let nextCheckboxId = 0;
/**
* Boolean checkbox following the shared AFRICANIES form field pattern.
*
* **Prefix/suffix slots are omitted** — a checkbox is a binary control with a
* label; affix projection does not apply.
*
* Custom painted control so the check glyph stays **white** on the mode
* primary fill (SFN green / STN orange).
*
* @example
* ```html
*
* ```
*/
@Component({
selector: 'africanies-checkbox',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
class: 'inline-flex max-w-full',
},
imports: [AfricaniesIconComponent],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CheckboxComponent),
multi: true,
},
],
template: `
@if (error(); as err) {
{{ err }}
} @else if (hint(); as h) {
{{ h }}
}
`,
})
/**
* Boolean checkbox following the shared AFRICANIES form field pattern.
*/
export class CheckboxComponent implements ControlValueAccessor {
private readonly modeColor = inject(ModeColorService);
protected readonly controlId = `africanies-checkbox-${++nextCheckboxId}`;
protected readonly hintId = `${this.controlId}-hint`;
protected readonly errorId = `${this.controlId}-error`;
protected readonly hintClass = FORM_HINT_CLASS;
protected readonly errorClass = FORM_ERROR_CLASS;
/** Visible label beside the checkbox. */
readonly label = input('');
/** Helper copy. Hidden while {@link error} is set. */
readonly hint = input(undefined);
/** Field-level validation message. Distinct from `ErrorStateComponent`. */
readonly error = input(null);
/** Checked state (`[(value)]` + CVA). */
readonly value = model(false);
/** Host disable flag (in addition to CVA). */
readonly disabledInput = input(false, {
alias: 'disabled',
transform: booleanAttribute,
});
protected readonly cvaDisabled = signal(false);
protected readonly disabled = computed(
() => this.disabledInput() || this.cvaDisabled(),
);
protected readonly markClass = computed(() => {
const base =
'pointer-events-none flex size-4 items-center justify-center rounded border text-white transition-colors ' +
FORM_FOCUS_VISIBLE_PEER_CLASS;
const err = this.error() ? ' !border-danger dark:!border-danger' : '';
if (this.value()) {
const c = this.modeColor.classes();
return `${base} ${c.bg} ${c.border}${err}`;
}
return `${base} border-neutral-400 bg-white dark:border-white/40 dark:bg-ink-950${err}`;
});
protected readonly describedBy = computed(() => {
if (this.error()) {
return this.errorId;
}
if (this.hint()) {
return this.hintId;
}
return null;
});
private onChange: (value: boolean) => void = () => undefined;
private onTouched: () => void = () => undefined;
/** @param value - Form model; non-boolean coerces via `!!`. */
writeValue(value: boolean | null): void {
this.value.set(!!value);
}
/** @param fn - User change callback. */
registerOnChange(fn: (value: boolean) => void): void {
this.onChange = fn;
}
/** @param fn - Blur callback. */
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
/** @param isDisabled - Blocks interaction when true. */
setDisabledState(isDisabled: boolean): void {
this.cvaDisabled.set(isDisabled);
}
protected onChangeEvent(event: Event): void {
const checked = (event.target as HTMLInputElement).checked;
this.value.set(checked);
this.onChange(checked);
}
protected onBlur(): void {
this.onTouched();
}
}