UNPKG

@sixbell-telco/sdk

Version:

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

325 lines (321 loc) 14.6 kB
import * as i0 from '@angular/core'; import { input, model, output, signal, computed, effect, Component } from '@angular/core'; import { toObservable, toSignal, takeUntilDestroyed } from '@angular/core/rxjs-interop'; import * as i1 from '@angular/forms'; import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; import { TranslatePipe } from '@ngx-translate/core'; import { TypographyDirective } from '@sixbell-telco/sdk/directives/typography'; import { cn } from '@sixbell-telco/sdk/utils/cn'; import { cva } from 'class-variance-authority'; import { Subject, switchMap, startWith, skip, debounceTime, distinctUntilChanged } from 'rxjs'; /* eslint-disable @typescript-eslint/no-explicit-any */ /** * @internal * Generates base textarea classes with style variants */ const textareaComponent = cva(['font-body', 'input', 'placeholder:placeholder-base-placeholder', 'leading-normal', 'grow', 'textarea', 'w-full'], { variants: { variant: { primary: ['textarea-primary'], secondary: ['textarea-secondary'], tertiary: ['textarea-tertiary'], accent: ['textarea-accent'], info: ['textarea-info'], success: ['textarea-success'], warning: ['textarea-warning'], error: ['textarea-error'], }, size: { xs: ['textarea-xs'], sm: ['textarea-sm'], md: ['textarea-md'], lg: ['textarea-lg'], xl: ['textarea-xl'], }, }, compoundVariants: [], defaultVariants: { variant: 'secondary', size: 'md', }, }); /** * A customizable textarea component with form integration and validation features * * @remarks * Supports character counting, debounced input, and automatic validation styling. * Implements ControlValueAccessor for Angular form compatibility. * * @example * ```html * <!-- Basic textarea usage --> * <st-textarea * [(value)]="description" * label="Description" * maxCharacters="500" * ></st-textarea> * ``` * * @example * ```html * <!-- Reactive form integration --> * <st-textarea * [parentForm]="userForm" * formControlName="bio" * variant="primary" * debounceTime="300" * ></st-textarea> * ``` */ class TextareaComponent { /** * Textarea style variant * @defaultValue 'secondary' */ variant = input(); /** * Textarea size variant * @defaultValue 'md' */ size = input(); /** * Whether to use ghost (transparent) styling * @defaultValue false */ ghost = input(false); /** * Label displayed above the textarea */ label = input(null); /** * HTML name attribute for the textarea */ name = input(null); /** * Placeholder text when empty */ placeholder = input(''); /** * Number of visible text rows * @defaultValue 3 */ rows = input(3); /** * Whether the textarea is readonly * @defaultValue false */ readonly = input(false); /** * Whether the textarea is disabled * @defaultValue false */ disabled = model(false); /** * Event emitted on textarea blur */ blurred = output(); /** @internal */ blurTrigger = signal(0); /** * Two-way bindable textarea value */ value = model(''); /** * Maximum allowed character count * @defaultValue 0 (unlimited) */ maxCharacters = input(0); /** * Whether current value exceeds maxCharacters * @internal */ invalidMaxCharacters = computed(() => this.value().length > this.maxCharacters()); /** * Parent form group for reactive forms */ parentForm = input(null); /** * Form control name for reactive forms */ formControlName = input(''); /** * @internal * Gets associated form control */ get formField() { if (!this.parentForm()) return null; return this.parentForm()?.get(this.formControlName()); } // ControlValueAccessor implementation /** @internal */ onControlChange = () => { }; /** @internal */ onControlTouch = () => { }; /** * Event emitted on Enter key press */ enterPressed = output(); /** * Debounce time in milliseconds for valueDebounced output * @defaultValue 500 */ debounceTime = input(500); /** * Event emitted after debounce time when value changes */ valueDebounced = output(); /** @internal */ debounceAction$ = new Subject(); /** @internal */ debounceTimeEffect = effect(() => { this.debounceAction$.next(this.debounceTime()); }); /** @internal */ value$ = toObservable(this.value); constructor() { this.setupDebounce(); } /** * @internal * Computed base textarea classes */ componentClass = computed(() => { return cn(textareaComponent({ variant: this.variant(), size: this.size(), }), { 'textarea-ghost': this.ghost() }); }); /** * @internal * Computed error state classes */ errorClass = computed(() => { return cn(textareaComponent({ variant: 'error', size: this.size(), }), { 'textarea-ghost': this.ghost() }); }); /** * @internal * Computed success state classes */ successClass = computed(() => { return cn(textareaComponent({ variant: 'success', size: this.size(), }), { 'textarea-ghost': this.ghost() }); }); /** * @internal * Computed character counter error classes */ errorClassMaxCharacters = computed(() => { return cn('grow-0 text-base-content ease-ease-out-back transition-colors duration-300', { 'text-error ease-ease-out-back transition-colors duration-300': this.invalidMaxCharacters(), }); }); // 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 input changes */ handleChange() { this.onControlChange(this.value()); } /** * @internal * Handles Enter key press */ handleEnter() { this.enterPressed.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); } /** @internal */ formControl = computed(() => this.parentForm()?.get(this.formControlName())); /** @internal */ formControl$ = toObservable(this.formControl); /** @internal */ statusChanges$ = this.formControl$.pipe(switchMap((control) => control?.statusChanges.pipe(startWith(control.status)) || [])); /** @internal */ stateChanges$ = this.formControl$.pipe(switchMap((control) => control?.valueChanges.pipe(startWith(control.value)) || [])); /** @internal */ statusSignal = toSignal(this.statusChanges$); /** @internal */ 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 * Sets up debounce mechanism for value changes */ setupDebounce() { this.debounceAction$ .pipe(switchMap((time) => this.value$.pipe(skip(1), debounceTime(time), distinctUntilChanged())), takeUntilDestroyed()) .subscribe((value) => { this.valueDebounced.emit(`${value}`); }); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: TextareaComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.0", type: TextareaComponent, isStandalone: true, selector: "st-textarea", 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 }, label: { classPropertyName: "label", publicName: "label", 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 }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, maxCharacters: { classPropertyName: "maxCharacters", publicName: "maxCharacters", 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 }, debounceTime: { classPropertyName: "debounceTime", publicName: "debounceTime", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { disabled: "disabledChange", blurred: "blurred", value: "valueChange", enterPressed: "enterPressed", valueDebounced: "valueDebounced" }, providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: TextareaComponent, 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\n\t<textarea\n\t\t[class]=\"validationClass()\"\n\t\t[attr.rows]=\"rows()\"\n\t\t[attr.name]=\"name()\"\n\t\t[attr.placeholder]=\"placeholder()\"\n\t\t[(ngModel)]=\"value\"\n\t\t[disabled]=\"disabled()\"\n\t\t(ngModelChange)=\"handleChange()\"\n\t\t(blur)=\"handleBlur()\"\n\t\t[readOnly]=\"readonly()\"\n\t></textarea>\n\n\t<div class=\"flex justify-between\">\n\t\t<div>\n\t\t\t<ng-content></ng-content>\n\t\t</div>\n\n\t\t@if (maxCharacters()) {\n\t\t\t<div [class]=\"errorClassMaxCharacters()\">\n\t\t\t\t<span typography [tyVariant]=\"'body-xs'\" [tyFontWeight]=\"'normal'\" tyColor=\"inherit\">\n\t\t\t\t\t{{ 'sdk.textarea.maxCharacters' | translate: { current: value().length, max: maxCharacters() } }}\n\t\t\t\t</span>\n\t\t\t</div>\n\t\t}\n\t</div>\n\n\t<!-- Hidden label for accessibility -->\n\t<label class=\"sr-only\" [for]=\"name()\">{{ labelVar }}</label>\n</fieldset>\n", dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { 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"] }, { kind: "directive", type: TypographyDirective, selector: "[typography]", inputs: ["tyVariant", "tyColor", "tyFontWeight", "tyTextOverflow"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: TextareaComponent, decorators: [{ type: Component, args: [{ selector: 'st-textarea', imports: [FormsModule, TypographyDirective, TranslatePipe], providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: TextareaComponent, 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\n\t<textarea\n\t\t[class]=\"validationClass()\"\n\t\t[attr.rows]=\"rows()\"\n\t\t[attr.name]=\"name()\"\n\t\t[attr.placeholder]=\"placeholder()\"\n\t\t[(ngModel)]=\"value\"\n\t\t[disabled]=\"disabled()\"\n\t\t(ngModelChange)=\"handleChange()\"\n\t\t(blur)=\"handleBlur()\"\n\t\t[readOnly]=\"readonly()\"\n\t></textarea>\n\n\t<div class=\"flex justify-between\">\n\t\t<div>\n\t\t\t<ng-content></ng-content>\n\t\t</div>\n\n\t\t@if (maxCharacters()) {\n\t\t\t<div [class]=\"errorClassMaxCharacters()\">\n\t\t\t\t<span typography [tyVariant]=\"'body-xs'\" [tyFontWeight]=\"'normal'\" tyColor=\"inherit\">\n\t\t\t\t\t{{ 'sdk.textarea.maxCharacters' | translate: { current: value().length, max: maxCharacters() } }}\n\t\t\t\t</span>\n\t\t\t</div>\n\t\t}\n\t</div>\n\n\t<!-- Hidden label for accessibility -->\n\t<label class=\"sr-only\" [for]=\"name()\">{{ labelVar }}</label>\n</fieldset>\n" }] }], ctorParameters: () => [] }); /** * Generated bundle index. Do not edit. */ export { TextareaComponent }; //# sourceMappingURL=sixbell-telco-sdk-components-forms-textarea.mjs.map