UNPKG

@sixbell-telco/sdk

Version:

A collection of reusable components designed for use in Sixbell Telco Angular projects

295 lines (291 loc) 14.8 kB
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