src/components/sq-textarea-form-control/sq-textarea-form-control.component.ts
Componente de textarea com Reactive Forms.
Substitui o componente legado sq-textarea com integração nativa de Reactive Forms,
ControlValueAccessor e Validator.
```html
<sq-textarea-form-control
[label]="'Descrição'"
[placeholder]="'Digite uma descrição...'"
[formControl]="descriptionControl"
></sq-textarea-form-control>```html
```html
<!-- Com validação required -->
<sq-textarea-form-control
[label]="'Comentário *'"
[formControl]="commentControl"
sqValidation
[fieldName]="'Comentário'"
></sq-textarea-form-control>
OnInit
OnDestroy
| changeDetection | ChangeDetectionStrategy.OnPush |
| providers |
{
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SqTextareaFormControlComponent), multi: true,
}
|
| selector | sq-textarea-form-control |
| standalone | true |
| imports |
NgClass
NgStyle
NgTemplateOutlet
ReactiveFormsModule
SqTooltipComponent
UniversalSafePipe
|
| styleUrls | ./sq-textarea-form-control.component.scss |
| templateUrl | ./sq-textarea-form-control.component.html |
Properties |
|
Methods |
Inputs |
Outputs |
| autoResize | |
Type : boolean
|
|
Default value : false
|
|
|
Se true, o textarea se expande automaticamente. |
|
| debounceTime | |
Type : number
|
|
Default value : 0
|
|
|
Debounce do valueChange em ms. |
|
| maxHeight | |
Type : string
|
|
Default value : '300px'
|
|
|
Altura máxima do textarea (quando autoResize=true). |
|
| minHeight | |
Type : string
|
|
Default value : '100px'
|
|
|
Altura mínima do textarea (quando autoResize=true). |
|
| rows | |
Type : number
|
|
Default value : 4
|
|
|
Número de linhas do textarea. |
|
| customClass | |
Type : string
|
|
Default value : ''
|
|
|
Inherited from
SqFormControlBaseDirective
|
|
|
Defined in
SqFormControlBaseDirective:55
|
|
|
Custom CSS class for the input element. |
|
| id | |
Type : string
|
|
|
Inherited from
SqFormControlBaseDirective
|
|
|
Defined in
SqFormControlBaseDirective:45
|
|
|
The id attribute for the input element. |
|
| label | |
Type : string
|
|
|
Inherited from
SqFormControlBaseDirective
|
|
|
Defined in
SqFormControlBaseDirective:50
|
|
|
An optional label for the input. |
|
| name | |
Type : string
|
|
Default value : `random-name-${(1 + Date.now() + Math.random()).toString().replace('.', '')}`
|
|
|
Inherited from
SqFormControlBaseDirective
|
|
|
Defined in
SqFormControlBaseDirective:40
|
|
|
The name attribute for the input element. |
|
| placeholder | |
Type : string
|
|
Default value : ''
|
|
|
Inherited from
SqFormControlBaseDirective
|
|
|
Defined in
SqFormControlBaseDirective:60
|
|
|
Placeholder text for the input element. |
|
| readonly | |
Type : boolean
|
|
Default value : false
|
|
|
Inherited from
SqFormControlBaseDirective
|
|
|
Defined in
SqFormControlBaseDirective:65
|
|
|
Flag to make the input element readonly. |
|
| tooltipColor | |
Type : string
|
|
Default value : 'inherit'
|
|
|
Inherited from
SqFormControlBaseDirective
|
|
|
Defined in
SqFormControlBaseDirective:80
|
|
|
Color of the tooltip. |
|
| tooltipIcon | |
Type : string
|
|
Default value : ''
|
|
|
Inherited from
SqFormControlBaseDirective
|
|
|
Defined in
SqFormControlBaseDirective:85
|
|
|
Icon for the tooltip. |
|
| tooltipMessage | |
Type : string
|
|
Default value : ''
|
|
|
Inherited from
SqFormControlBaseDirective
|
|
|
Defined in
SqFormControlBaseDirective:70
|
|
|
Tooltip message to display. |
|
| tooltipPlacement | |
Type : "center top" | "center bottom" | "left center" | "right center"
|
|
Default value : 'right center'
|
|
|
Inherited from
SqFormControlBaseDirective
|
|
|
Defined in
SqFormControlBaseDirective:75
|
|
|
Placement of the tooltip. |
|
| blurred | |
Type : EventEmitter<FocusEvent>
|
|
|
Inherited from
SqFormControlBaseDirective
|
|
|
Defined in
SqFormControlBaseDirective:100
|
|
|
Event emitter for blur events. |
|
| focused | |
Type : EventEmitter<FocusEvent>
|
|
|
Inherited from
SqFormControlBaseDirective
|
|
|
Defined in
SqFormControlBaseDirective:95
|
|
|
Event emitter for focus events. |
|
| valueChange | |
Type : EventEmitter<any>
|
|
|
Inherited from
SqFormControlBaseDirective
|
|
|
Defined in
SqFormControlBaseDirective:90
|
|
|
Event emitter for input value changes. |
|
| onInput | ||||||||
onInput(event: Event)
|
||||||||
|
Handler para auto-resize.
Parameters :
Returns :
void
|
| watchValueChanges |
watchValueChanges()
|
|
Inherited from
SqFormControlBaseDirective
|
|
Defined in
SqFormControlBaseDirective:136
|
|
Override do watchValueChanges para adicionar debounce.
Returns :
void
|
| ngOnDestroy |
ngOnDestroy()
|
|
Inherited from
SqFormControlBaseDirective
|
|
Defined in
SqFormControlBaseDirective:136
|
|
Cleanup on component destruction.
Returns :
void
|
| ngOnInit |
ngOnInit()
|
|
Inherited from
SqFormControlBaseDirective
|
|
Defined in
SqFormControlBaseDirective:129
|
|
Lifecycle hook that sets up value change subscriptions with optional debounce.
Returns :
void
|
| onBlur | ||||||||
onBlur(event: FocusEvent)
|
||||||||
|
Inherited from
SqFormControlBaseDirective
|
||||||||
|
Defined in
SqFormControlBaseDirective:198
|
||||||||
|
Handle blur events. Marks the control as touched when the user leaves the input.
Parameters :
Returns :
void
|
| onFocus | ||||||||
onFocus(event: FocusEvent)
|
||||||||
|
Inherited from
SqFormControlBaseDirective
|
||||||||
|
Defined in
SqFormControlBaseDirective:208
|
||||||||
|
Handle focus events. Emits the focused event when the input receives focus.
Parameters :
Returns :
void
|
| registerOnChange | ||||||||
registerOnChange(fn: any)
|
||||||||
|
Inherited from
SqFormControlBaseDirective
|
||||||||
|
Defined in
SqFormControlBaseDirective:153
|
||||||||
|
ControlValueAccessor: Registers a callback function that is called when the control's value changes.
Parameters :
Returns :
void
|
| registerOnTouched | ||||||||
registerOnTouched(fn: any)
|
||||||||
|
Inherited from
SqFormControlBaseDirective
|
||||||||
|
Defined in
SqFormControlBaseDirective:161
|
||||||||
|
ControlValueAccessor: Registers a callback function that is called when the control is touched.
Parameters :
Returns :
void
|
| setDisabledState | ||||||||
setDisabledState(isDisabled: boolean)
|
||||||||
|
Inherited from
SqFormControlBaseDirective
|
||||||||
|
Defined in
SqFormControlBaseDirective:169
|
||||||||
|
ControlValueAccessor: Sets the disabled state of the control.
Parameters :
Returns :
void
|
| writeValue | ||||||||
writeValue(value: any)
|
||||||||
|
Inherited from
SqFormControlBaseDirective
|
||||||||
|
Defined in
SqFormControlBaseDirective:145
|
||||||||
|
ControlValueAccessor: Writes a new value to the element.
Parameters :
Returns :
void
|
| Private cdr |
Default value : inject(ChangeDetectorRef)
|
|
Referência ao ChangeDetectorRef. |
| Private elementRef |
Default value : inject(ElementRef)
|
|
Referência ao ElementRef. |
| labelTemplate |
Type : TemplateRef<HTMLElement> | null
|
Default value : null
|
Decorators :
@ContentChild('labelTemplate')
|
|
Template customizado para o label. |
| leftLabel |
Type : TemplateRef<HTMLElement> | null
|
Default value : null
|
Decorators :
@ContentChild('leftLabel')
|
|
Template para label à esquerda. |
| rightLabel |
Type : TemplateRef<HTMLElement> | null
|
Default value : null
|
Decorators :
@ContentChild('rightLabel')
|
|
Template para label à direita. |
| control |
Default value : new FormControl<any>(null)
|
|
Inherited from
SqFormControlBaseDirective
|
|
Defined in
SqFormControlBaseDirective:105
|
|
Internal FormControl for managing the input value and state. |
| Protected destroy$ |
Default value : new Subject<void>()
|
|
Inherited from
SqFormControlBaseDirective
|
|
Defined in
SqFormControlBaseDirective:110
|
|
Subject for managing subscriptions. |
| Protected onChange |
Type : function
|
Default value : () => {...}
|
|
Inherited from
SqFormControlBaseDirective
|
|
Defined in
SqFormControlBaseDirective:117
|
|
ControlValueAccessor callback function called when the value changes. Registered via registerOnChange(). |
| Protected onTouched |
Type : function
|
Default value : () => {...}
|
|
Inherited from
SqFormControlBaseDirective
|
|
Defined in
SqFormControlBaseDirective:124
|
|
ControlValueAccessor callback function called when the control is touched. Registered via registerOnTouched(). |
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ContentChild,
ElementRef,
forwardRef,
inject,
Input,
OnDestroy,
OnInit,
TemplateRef,
} from '@angular/core';
import { NgClass, NgStyle, NgTemplateOutlet } from '@angular/common';
import {
NG_VALUE_ACCESSOR,
ReactiveFormsModule,
} from '@angular/forms';
import { debounceTime, takeUntil } from 'rxjs/operators';
import { SqTooltipComponent } from '../sq-tooltip/sq-tooltip.component';
import { UniversalSafePipe } from '../../pipes/universal-safe/universal-safe.pipe';
import { SqFormControlBaseDirective } from '../../directives/sq-form-control-base';
/**
* Componente de textarea com Reactive Forms.
*
* Substitui o componente legado `sq-textarea` com integração nativa de Reactive Forms,
* ControlValueAccessor e Validator.
*
* @example
* ```html
* <sq-textarea-form-control
* [label]="'Descrição'"
* [placeholder]="'Digite uma descrição...'"
* [formControl]="descriptionControl"
* ></sq-textarea-form-control>
* ```
*
* @example
* ```html
* <!-- Com validação required -->
* <sq-textarea-form-control
* [label]="'Comentário *'"
* [formControl]="commentControl"
* sqValidation
* [fieldName]="'Comentário'"
* ></sq-textarea-form-control>
* ```
*/
@Component({
selector: 'sq-textarea-form-control',
templateUrl: './sq-textarea-form-control.component.html',
styleUrls: ['./sq-textarea-form-control.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [NgClass, NgStyle, NgTemplateOutlet, ReactiveFormsModule, SqTooltipComponent, UniversalSafePipe],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SqTextareaFormControlComponent),
multi: true,
},
],
})
export class SqTextareaFormControlComponent extends SqFormControlBaseDirective implements OnInit, OnDestroy {
// ============================================================
// Inputs - Aparência
// ============================================================
/**
* Número de linhas do textarea.
*/
@Input() rows = 4;
/**
* Se true, o textarea se expande automaticamente.
*/
@Input() autoResize = false;
/**
* Altura mínima do textarea (quando autoResize=true).
*/
@Input() minHeight = '100px';
/**
* Altura máxima do textarea (quando autoResize=true).
*/
@Input() maxHeight = '300px';
// ============================================================
// Inputs - Comportamento
// ============================================================
/**
* Debounce do valueChange em ms.
*/
@Input() debounceTime = 0;
// ============================================================
// Templates
// ============================================================
/**
* Template para label à esquerda.
*/
@ContentChild('leftLabel') leftLabel: TemplateRef<HTMLElement> | null = null;
/**
* Template para label à direita.
*/
@ContentChild('rightLabel') rightLabel: TemplateRef<HTMLElement> | null = null;
/**
* Template customizado para o label.
*/
@ContentChild('labelTemplate') labelTemplate: TemplateRef<HTMLElement> | null = null;
// ============================================================
// Estado interno
// ============================================================
/**
* Referência ao ChangeDetectorRef.
*/
private cdr = inject(ChangeDetectorRef);
/**
* Referência ao ElementRef.
*/
private elementRef = inject(ElementRef);
/**
* Override do watchValueChanges para adicionar debounce.
*/
override watchValueChanges(): void {
if (this.debounceTime > 0) {
this.control.valueChanges
.pipe(debounceTime(this.debounceTime), takeUntil(this.destroy$))
.subscribe(value => {
this.onChange(value);
this.valueChange.emit(value);
});
} else {
super.watchValueChanges();
}
}
// ============================================================
// Métodos públicos - Eventos
// ============================================================
/**
* Handler para auto-resize.
*
* @param event - Evento de input.
*/
onInput(event: Event): void {
if (this.autoResize) {
const textarea = event.target as HTMLTextAreaElement;
textarea.style.height = 'auto';
textarea.style.height = `${textarea.scrollHeight}px`;
}
}
// ============================================================
// Métodos privados
// ============================================================
}
<div class="sq-textarea-form-control {{ customClass }}">
<!-- Label -->
@if (label?.length || tooltipMessage || labelTemplate) {
<div
class="display-flex align-items-center wrapper-label-input"
[ngClass]="{ disabled: disabled, readonly: readonly }"
>
@if (label && !labelTemplate) {
<label class="label-input mr-1" [for]="id" [innerHtml]="label | universalSafe"></label>
}
@if (labelTemplate) {
<ng-container *ngTemplateOutlet="labelTemplate"></ng-container>
}
@if (tooltipMessage) {
<sq-tooltip
[message]="tooltipMessage"
[placement]="tooltipPlacement"
[color]="tooltipColor"
[icon]="tooltipIcon"
></sq-tooltip>
}
</div>
}
<!-- Container do textarea -->
<div
class="textarea-container"
[ngClass]="{
disabled: disabled,
readonly: readonly,
}"
>
<!-- Left label -->
@if (leftLabel) {
<span class="input-group-text left">
<ng-container *ngTemplateOutlet="leftLabel"></ng-container>
</span>
}
<!-- Textarea -->
<textarea
class="textarea scrollbar"
[ngStyle]="{
'min-height': autoResize ? minHeight : null,
'max-height': autoResize ? maxHeight : null,
}"
[ngClass]="{
disabled: disabled,
readonly: readonly,
'auto-resize': autoResize,
}"
[id]="id"
[name]="name"
[placeholder]="placeholder"
[rows]="rows"
[disabled]="disabled"
[readonly]="readonly"
[formControl]="control"
(blur)="onBlur($event)"
(focus)="onFocus($event)"
(input)="onInput($event)"
></textarea>
<!-- Right label -->
@if (rightLabel) {
<span class="input-group-text right">
<ng-container *ngTemplateOutlet="rightLabel"></ng-container>
</span>
}
</div>
</div>
./sq-textarea-form-control.component.scss
.sq-textarea-form-control {
position: relative;
display: block;
}
// Label wrapper
.wrapper-label-input {
margin-bottom: 0.25rem;
&.disabled {
opacity: 0.6;
}
&.readonly {
opacity: 0.8;
}
.label-input {
font-size: 0.875rem;
font-weight: 500;
color: var(--text_color, #333);
}
}
// Container do textarea
.textarea-container {
position: relative;
display: flex;
border: 1px solid var(--border_color, #ccc);
border-radius: 5px;
background-color: var(--background, #fff);
transition: border-color 0.2s ease, box-shadow 0.2s ease;
&:focus-within:not(.disabled):not(.readonly) {
border-color: var(--primary_color, #007bff);
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.15);
}
&.error {
border-color: var(--color_error, #dc3545);
&:focus-within {
box-shadow: 0 0 0 2px rgba(220, 53, 69, 0.15);
}
}
&.disabled {
opacity: 0.6;
background-color: var(--color_bg_input_disabled, #f5f5f5);
cursor: not-allowed;
}
&.readonly {
background-color: var(--color_bg_input_readonly, #fafafa);
}
}
// Textarea
.textarea {
flex: 1;
width: 100%;
min-height: 100px;
padding: 0.625rem 1rem;
border: none;
border-radius: 5px;
font-family: inherit;
font-size: 0.875rem;
line-height: 1.5;
color: var(--text_color, #333);
background-color: transparent;
resize: vertical;
outline: none;
transition: background-color 0.2s ease;
&::placeholder {
color: var(--color_text-icon_neutral_tertiary, #999);
}
&.disabled {
cursor: not-allowed;
resize: none;
}
&.readonly {
cursor: default;
resize: none;
}
&.has-icon-external {
padding-right: 2.5rem;
}
&.auto-resize {
resize: none;
overflow: hidden;
}
}
// Input group text (left/right labels)
.input-group-text {
display: flex;
align-items: flex-start;
padding: 0.625rem 0.75rem;
background-color: var(--color_bg_box_neutral_secondary, #f5f5f5);
border: none;
font-size: 0.875rem;
color: var(--text_color, #333);
&.left {
border-radius: 5px 0 0 5px;
border-right: 1px solid var(--border_color, #ccc);
}
&.right {
border-radius: 0 5px 5px 0;
border-left: 1px solid var(--border_color, #ccc);
}
}
// External icon
.icon-external {
position: absolute;
right: 1rem;
top: 0.625rem;
font-size: 1rem;
color: var(--color_text-icon_neutral_tertiary, #666);
&.no-label {
top: 0.625rem;
}
}
// Validation box
.box-validation {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 1.25rem;
margin-top: 0.25rem;
font-size: 0.75rem;
color: var(--color_error, #dc3545);
i {
margin-right: 0.25rem;
}
.error-text {
flex: 1;
}
}
// Max length counter
.max-length-counter {
margin-left: auto;
font-size: 0.75rem;
color: var(--color_text-icon_neutral_tertiary, #666);
font-weight: 500;
&.warning {
color: var(--color_warning, #ffc107);
}
&.danger {
color: var(--color_error, #dc3545);
}
}
.visibility-hidden-force {
visibility: hidden;
}