Textarea
The <smart-textarea> component renders a multi-line text input with two-way value binding, optional placeholder, disabled state, label, action buttons, avatar/toolbar/preview/footer slots, and a structured actionClick output. It follows the Base + Standard + Wrapper pattern with an InjectionToken-based extension mechanism. The abstract TextareaBaseComponent defines the shared API — value (two-way ModelSignal<string>), placeholder, disabled, optional ITextareaOptions, cssClass (alias class), and the actionClick output. TextareaStandardComponent is a barebones placeholder using a native <textarea> element. TextareaPresetComponent is the styled Tailwind variation that honours every option, including variant and autoFocus. TextareaComponent is the public wrapper that renders TextareaStandardComponent by default and accepts a custom replacement (such as the preset) via TEXTAREA_STANDARD_COMPONENT_TOKEN.
Usage
<smart-textarea
[options]="options"
[(value)]="comment"
placeholder="Write a comment..."
(actionClick)="onActionClick($event)"
/>
Components
TextareaComponent (<smart-textarea>)
Main wrapper. Delegates to TextareaStandardComponent by default. When TEXTAREA_STANDARD_COMPONENT_TOKEN is provided, renders the injected component via NgComponentOutlet. Re-emits actionClick.
TextareaStandardComponent (<smart-textarea-standard>)
Barebones placeholder using a native <textarea>. Renders an outer wrapper, an optional <label> (when options.label provided), optional avatar/toolbar slots, the <textarea> with [value] two-way bound through input event, [disabled], [rows] (default 3), [attr.maxlength], [attr.placeholder], [attr.aria-label], and an optional row of <button class="action variant-{variant}"> per options.actions entry that emits { actionId, value } via actionClick (suppressed when disabled). Optional previewTpl and footerTpl slots render below the actions row. The external cssClass is applied to the root wrapper.
TextareaPresetComponent (<smart-textarea-preset>)
Styled variation that extends TextareaBaseComponent and is a drop-in replacement for TextareaStandardComponent. See Preset.
TextareaBaseComponent (abstract)
Abstract base directive. Exposes:
value: ModelSignal<string>(default'')placeholder: InputSignal<string>(default'')disabled: InputSignal<boolean>(defaultfalse)options: InputSignal<ITextareaOptions | undefined>cssClass: InputSignal<string>(aliasclass)actionClick: OutputEmitterRef<ITextareaActionClick>
ITextareaActionClick = { actionId: string; value: string }.
API
Inputs
| Input | Type | Default | Description |
|---|---|---|---|
value | ModelSignal<string> | '' | Two-way bindable textarea value |
placeholder | InputSignal<string> | '' | Placeholder text |
disabled | InputSignal<boolean> | false | Disabled state |
options | InputSignal<ITextareaOptions | undefined> | - | Optional configuration |
class | InputSignal<string> | '' | External CSS classes (alias for cssClass) |
Outputs
| Output | Type | Description |
|---|---|---|
actionClick | OutputEmitterRef<ITextareaActionClick> | Emitted when an action button is clicked |
Preset
TextareaPresetComponent (selector smart-textarea-preset) renders the Tailwind UI comment-form look: white / gray-900-friendly surfaces (bg-white, dark:bg-white/5), gray-300 / white/10 outlines with a blue focus ring, gray-900 / white text, gray-400 / gray-500 placeholders and rounded-lg fields. Every class is smart:-prefixed with an explicit dark: variant; the recipes live in preset/preset-classes.util.ts.
It renders everything the standard component renders (label, avatar, toolbar, field, actions, preview, footer) and additionally honours the options the standard ignores:
variantpicks the layout (default'simple', exposed asdata-varianton the root):Variant Look simpleOutlined, rounded field; toolbar (left) and actions (right) in a row below it with-avatar-actionsBorderless field inside an outlined box whose focus ring follows the field; toolbar + actions inside it with-underlineBorderless field on a bottom border that thickens to blue on focus; toolbar + actions below with-pill-actionsOutlined box with a divided ( border-t) bar inside; action buttons arerounded-fullpillswith-previewWrite / Preview tabs above the field; the Preview tab swaps the field for previewTplOutside
with-preview, apreviewTplrenders as agray-50/white/5block below the field.autoFocusfocuses the field after the first render (and sets theautofocusattribute).maxLengthis applied asmaxlengthand also shows acount/maxcounter under the field.requiredsetsrequiredon the field and a red*in the label; the label is linked to the field withfor/id.avatarTplrenders in a column to the left of the body in every variant.Actions are styled per
action.variant:primary(solid blue),secondary(default: white with agray-300ring,white/10in dark mode),ghost(text only, gray hover). They are disabled with the field, and clicking one emitsactionClickwith{ actionId, value }(never while disabled).
Register it on the token to restyle every <smart-textarea>:
Because
TextareaComponentrenders injected components viaNgComponentOutlet(which passes inputs by canonical name),TextareaPresetComponentoverridescssClassasinput<string>('')without theclassalias. Bind it as[cssClass]when using the<smart-textarea-preset>selector directly, or just passclasson<smart-textarea>(the wrapper forwards it).
Extending the base class
import {
ChangeDetectionStrategy,
Component,
computed,
input,
ViewEncapsulation,
} from '@angular/core';
import {
ITextareaOptions,
TEXTAREA_STANDARD_COMPONENT_TOKEN,
TextareaBaseComponent,
TextareaComponent,
} from '@smartsoft001/angular';
@Component({
selector: 'docs-custom-textarea',
template: `
<div [class]="containerClasses()">
@if (options()?.label) {
<label
class="docs-textarea__label"
[attr.for]="options()?.name ?? null"
>
{{ options()!.label }}
@if (options()?.required) {
<span class="docs-textarea__required">*</span>
}
</label>
}
<textarea
class="docs-textarea__field"
[attr.id]="options()?.name ?? null"
[attr.name]="options()?.name ?? null"
[attr.placeholder]="placeholder() || null"
[attr.maxlength]="options()?.maxLength ?? null"
[rows]="options()?.rows ?? 3"
[disabled]="disabled()"
[value]="value()"
(input)="onInput($event)"
></textarea>
@if ((options()?.actions ?? []).length > 0) {
<div class="docs-textarea__actions">
@for (action of options()!.actions!; track action.id) {
<button
type="button"
class="docs-textarea__action"
[attr.data-variant]="action.variant ?? 'secondary'"
[disabled]="disabled()"
(click)="onActionClick(action.id)"
>
{{ action.label ?? action.id }}
</button>
}
</div>
}
</div>
`,
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomTextareaComponent extends TextareaBaseComponent {
// NgComponentOutlet passes 'cssClass' by canonical name, not the 'class' alias.
override cssClass = input<string>('');
containerClasses = computed(() =>
[
'docs-textarea',
`docs-textarea--${this.options()?.variant ?? 'simple'}`,
this.cssClass(),
]
.filter(Boolean)
.join(' '),
);
// `value` is a model on the base, so writing to it keeps the two-way binding
// of `<smart-textarea [(value)]="...">` working for the standard component.
protected onInput(event: Event): void {
this.value.set((event.target as HTMLTextAreaElement).value);
}
protected onActionClick(actionId: string): void {
if (this.disabled()) return;
this.actionClick.emit({ actionId, value: this.value() });
}
}
@Component({
selector: 'docs-textarea-custom-example',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [TextareaComponent],
// The token swaps the standard textarea for the custom one everywhere below
// this component, so consumers keep writing `<smart-textarea>`.
providers: [
{
provide: TEXTAREA_STANDARD_COMPONENT_TOKEN,
useValue: CustomTextareaComponent,
},
],
// NgComponentOutlet forwards inputs only: the wrapper's (actionClick) and the
// [(value)] write-back stay silent, so `comment` below is the initial value.
template: `
<smart-textarea
[value]="comment"
placeholder="Add your comment..."
[options]="options"
/>
`,
})
export class TextareaCustomExampleComponent {
comment = 'Looks good to me.';
options: ITextareaOptions = {
label: 'Comment',
name: 'comment',
rows: 4,
maxLength: 280,
required: true,
variant: 'with-pill-actions',
actions: [
{ id: 'cancel', label: 'Cancel', variant: 'ghost' },
{ id: 'submit', label: 'Send', variant: 'primary' },
],
};
}
Source
The component lives in packages/shared/angular/src/lib/components/textarea and is documented for Claude Code by the angular-components-textarea skill.