@sixbell-telco/sdk
Version:
A collection of reusable components designed for use in Sixbell Telco Angular projects
295 lines (291 loc) • 14.8 kB
JavaScript
import * as i0 from '@angular/core';
import { inject, input, model, output, computed, signal, Component } from '@angular/core';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import * as i1 from '@angular/forms';
import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';
import { cn } from '@sixbell-telco/sdk/utils/cn';
import { cva } from 'class-variance-authority';
import { switchMap, startWith } from 'rxjs';
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* @internal
* Generates base select classes with style variants
*/
const selectComponent = cva(['w-full', 'min-w-fit', 'text-pretty', 'font-body', 'font-normal', 'select', 'leading-normal'], {
variants: {
variant: {
primary: ['select-primary'],
secondary: ['select-secondary'],
tertiary: ['select-tertiary'],
accent: ['select-accent'],
info: ['select-info'],
success: ['select-success'],
error: ['select-error'],
warning: ['select-warning'],
},
size: {
xs: ['select-xs'],
sm: ['select-sm'],
md: ['select-md'],
lg: ['select-lg'],
xl: ['select-xl'],
},
},
compoundVariants: [],
defaultVariants: {
variant: 'secondary',
size: 'md',
},
});
/**
* A customizable select dropdown component with form integration
*
* @remarks
* Supports both primitive and object options with configurable tracking properties.
* Implements ControlValueAccessor for Angular form compatibility.
*
* @example
* ```html
* <!-- Basic usage with primitive values -->
* <st-select
* [options]="['Option 1', 'Option 2']"
* [(value)]="selectedValue"
* ></st-select>
* ```
*
* @example
* ```html
* <!-- Object options with custom tracking -->
* <st-select
* [options]="users"
* trackBy="id"
* key="name"
* label="Select user"
* [parentForm]="userForm"
* formControlName="selectedUser"
* ></st-select>
* ```
*/
class SelectComponent {
/** @internal Translation service instance */
translateService = inject(TranslateService);
/**
* Select dropdown style variant
* @defaultValue 'secondary'
*/
variant = input('secondary');
/**
* Select dropdown size variant
* @defaultValue 'md'
*/
size = input('md');
/**
* Whether to use ghost (transparent) styling
* @defaultValue false
*/
ghost = input(false);
/**
* HTML name attribute for the select
*/
name = input(null);
/**
* Placeholder text when no option is selected
*/
placeholder = input('');
/**
* Label text displayed above the select
*/
label = input('');
/**
* Two-way bindable selected value
*/
value = model();
/**
* Array of available options (primitives or objects)
*/
options = input([]);
/**
* Text to display when no options are available
*/
emptyOptionText = input();
/**
* Property name for tracking object options
* @defaultValue 'id'
*/
trackBy = input('id');
/**
* Property name for displaying object options
* @defaultValue 'name'
*/
key = input('name');
/**
* Parent form group for reactive forms
*/
parentForm = input(null);
/**
* Form control name for reactive forms
*/
formControlName = input('');
/**
* Event emitted when select loses focus
*/
blurred = output();
// ControlValueAccessor implementation
onControlChange = () => { };
onControlTouch = () => { };
/**
* Whether the select is disabled
* @defaultValue false
*/
disabled = model(false);
/**
* Event emitted when selection changes
*/
valueUpdated = output();
/**
* @internal
* Checks if options are primitive values
*/
simpleOption = computed(() => {
return this.options().every((option) => typeof option !== 'object');
});
/**
* @internal
* Computed base select classes
*/
componentClass = computed(() => {
return cn(selectComponent({
variant: this.variant(),
size: this.size(),
}), { 'select-ghost': this.ghost() });
});
/**
* @internal
* Computed error state classes
*/
errorClass = computed(() => {
return cn(selectComponent({
variant: 'error',
size: this.size(),
}), { 'select-ghost': this.ghost() });
});
/**
* @internal
* Computed success state classes
*/
successClass = computed(() => {
return cn(selectComponent({
variant: 'success',
size: this.size(),
}), { 'select-ghost': this.ghost() });
});
// ControlValueAccessor methods
writeValue(obj) {
this.value.set(obj);
}
registerOnChange(fn) {
this.onControlChange = fn;
}
registerOnTouched(fn) {
this.onControlTouch = fn;
}
setDisabledState(isDisabled) {
this.disabled.set(isDisabled);
}
/**
* @internal
* Handles selection changes
*/
handleSelect() {
this.valueUpdated.emit(this.value());
this.onControlChange(this.value());
}
/**
* @internal
* Handles blur event
*/
handleBlur() {
this.blurred.emit(this.value());
this.onControlTouch();
this.blurTrigger.update((v) => v + 1);
}
// Validation and form integration
blurTrigger = signal(0);
formControl = computed(() => this.parentForm()?.get(this.formControlName()));
formControl$ = toObservable(this.formControl);
statusChanges$ = this.formControl$.pipe(switchMap((control) => control?.statusChanges.pipe(startWith(control.status)) || []));
stateChanges$ = this.formControl$.pipe(switchMap((control) => control?.valueChanges.pipe(startWith(control.value)) || []));
statusSignal = toSignal(this.statusChanges$);
stateSignal = toSignal(this.stateChanges$);
/**
* @internal
* Computed validation classes based on form state
*/
validationClass = computed(() => {
const control = this.formControl();
const trigger = this.blurTrigger();
if (!control)
return this.componentClass();
const isTouched = control.touched || trigger > 0;
this.statusSignal();
this.stateSignal();
if (control.dirty || isTouched) {
if (control.invalid)
return this.errorClass();
if (control.valid)
return this.successClass();
}
return this.componentClass();
});
/**
* @internal
* Validates trackBy and key configuration
*/
isValidTrackAndKey = computed(() => {
const opts = this.options();
if (!opts || opts.length === 0) {
// Nothing to validate – treat as valid.
return true;
}
const trackProp = this.trackBy(); // e.g. 'id'
const keyProp = this.key(); // e.g. 'name'
// Validate that each option has a non-empty value for both properties.
const allHaveValues = opts.every((o) => o && o[trackProp] != null && o[trackProp] !== '' && o[keyProp] != null && o[keyProp] !== '');
if (!allHaveValues) {
console.warn('[st-select] Provided trackBy ("' +
this.trackBy() +
'") and/or key ("' +
this.key() +
'") property is missing or not unique in the options. ' +
'Falling back to tracking by identity. For better performance, please ensure each option has a unique property for tracking and a valid property for display.');
return false;
}
// Check that the tracking property values are unique.
const trackValues = opts.map((o) => o[trackProp]);
return trackValues.length === new Set(trackValues).size;
});
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: SelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.0", type: SelectComponent, isStandalone: true, selector: "st-select", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, ghost: { classPropertyName: "ghost", publicName: "ghost", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, emptyOptionText: { classPropertyName: "emptyOptionText", publicName: "emptyOptionText", isSignal: true, isRequired: false, transformFunction: null }, trackBy: { classPropertyName: "trackBy", publicName: "trackBy", isSignal: true, isRequired: false, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: false, transformFunction: null }, parentForm: { classPropertyName: "parentForm", publicName: "parentForm", isSignal: true, isRequired: false, transformFunction: null }, formControlName: { classPropertyName: "formControlName", publicName: "formControlName", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", blurred: "blurred", disabled: "disabledChange", valueUpdated: "valueUpdated" }, providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: SelectComponent,
multi: true,
},
], ngImport: i0, template: "@let labelVar = label();\n\n<fieldset class=\"fieldset p-0\">\n\t@if (labelVar) {\n\t\t<legend class=\"fieldset-legend pt-0\">{{ labelVar }}</legend>\n\t}\n\t<div [class]=\"validationClass()\">\n\t\t<!-- select field -->\n\t\t<select\n\t\t\tclass=\"w-full min-w-fit grow\"\n\t\t\t[attr.name]=\"name()\"\n\t\t\t[(ngModel)]=\"value\"\n\t\t\t[disabled]=\"disabled()\"\n\t\t\t(ngModelChange)=\"handleSelect()\"\n\t\t\t(blur)=\"handleBlur()\"\n\t\t>\n\t\t\t@if (emptyOptionText()) {\n\t\t\t\t<option value=\"\">\n\t\t\t\t\t{{ emptyOptionText() }}\n\t\t\t\t</option>\n\t\t\t}\n\n\t\t\t@if (simpleOption()) {\n\t\t\t\t@for (option of options(); track option) {\n\t\t\t\t\t<option [ngValue]=\"option\">\n\t\t\t\t\t\t{{ option }}\n\t\t\t\t\t</option>\n\t\t\t\t}\n\t\t\t} @else {\n\t\t\t\t@if (isValidTrackAndKey()) {\n\t\t\t\t\t@for (option of options(); track option[trackBy()]) {\n\t\t\t\t\t\t<option [ngValue]=\"option\">\n\t\t\t\t\t\t\t{{ option[key()] }}\n\t\t\t\t\t\t</option>\n\t\t\t\t\t}\n\t\t\t\t} @else {\n\t\t\t\t\t@for (option of options(); track option) {\n\t\t\t\t\t\t<option [ngValue]=\"option\">\n\t\t\t\t\t\t\t{{ option }}\n\t\t\t\t\t\t</option>\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t</select>\n\t</div>\n\n\t<!-- Hidden label for accessibility -->\n\t<label class=\"sr-only\" [for]=\"name()\">{{ labelVar }}</label>\n\n\t<!-- Additional content -->\n\t<ng-content></ng-content>\n</fieldset>\n", dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: SelectComponent, decorators: [{
type: Component,
args: [{ selector: 'st-select', imports: [FormsModule], providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: SelectComponent,
multi: true,
},
], template: "@let labelVar = label();\n\n<fieldset class=\"fieldset p-0\">\n\t@if (labelVar) {\n\t\t<legend class=\"fieldset-legend pt-0\">{{ labelVar }}</legend>\n\t}\n\t<div [class]=\"validationClass()\">\n\t\t<!-- select field -->\n\t\t<select\n\t\t\tclass=\"w-full min-w-fit grow\"\n\t\t\t[attr.name]=\"name()\"\n\t\t\t[(ngModel)]=\"value\"\n\t\t\t[disabled]=\"disabled()\"\n\t\t\t(ngModelChange)=\"handleSelect()\"\n\t\t\t(blur)=\"handleBlur()\"\n\t\t>\n\t\t\t@if (emptyOptionText()) {\n\t\t\t\t<option value=\"\">\n\t\t\t\t\t{{ emptyOptionText() }}\n\t\t\t\t</option>\n\t\t\t}\n\n\t\t\t@if (simpleOption()) {\n\t\t\t\t@for (option of options(); track option) {\n\t\t\t\t\t<option [ngValue]=\"option\">\n\t\t\t\t\t\t{{ option }}\n\t\t\t\t\t</option>\n\t\t\t\t}\n\t\t\t} @else {\n\t\t\t\t@if (isValidTrackAndKey()) {\n\t\t\t\t\t@for (option of options(); track option[trackBy()]) {\n\t\t\t\t\t\t<option [ngValue]=\"option\">\n\t\t\t\t\t\t\t{{ option[key()] }}\n\t\t\t\t\t\t</option>\n\t\t\t\t\t}\n\t\t\t\t} @else {\n\t\t\t\t\t@for (option of options(); track option) {\n\t\t\t\t\t\t<option [ngValue]=\"option\">\n\t\t\t\t\t\t\t{{ option }}\n\t\t\t\t\t\t</option>\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t</select>\n\t</div>\n\n\t<!-- Hidden label for accessibility -->\n\t<label class=\"sr-only\" [for]=\"name()\">{{ labelVar }}</label>\n\n\t<!-- Additional content -->\n\t<ng-content></ng-content>\n</fieldset>\n" }]
}] });
/**
* Generated bundle index. Do not edit.
*/
export { SelectComponent };
//# sourceMappingURL=sixbell-telco-sdk-components-forms-select.mjs.map