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> (default false)
  • options: InputSignal<ITextareaOptions | undefined>
  • cssClass: InputSignal<string> (alias class)
  • actionClick: OutputEmitterRef<ITextareaActionClick>

ITextareaActionClick = { actionId: string; value: string }.

API

Inputs

InputTypeDefaultDescription
valueModelSignal<string>''Two-way bindable textarea value
placeholderInputSignal<string>''Placeholder text
disabledInputSignal<boolean>falseDisabled state
optionsInputSignal<ITextareaOptions | undefined>-Optional configuration
classInputSignal<string>''External CSS classes (alias for cssClass)

Outputs

OutputTypeDescription
actionClickOutputEmitterRef<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:

  • variant picks the layout (default 'simple', exposed as data-variant on the root):

    VariantLook
    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 are rounded-full pills
    with-previewWrite / Preview tabs above the field; the Preview tab swaps the field for previewTpl

    Outside with-preview, a previewTpl renders as a gray-50 / white/5 block below the field.

  • autoFocus focuses the field after the first render (and sets the autofocus attribute).

  • maxLength is applied as maxlength and also shows a count/max counter under the field.

  • required sets required on the field and a red * in the label; the label is linked to the field with for/id.

  • avatarTpl renders 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 a gray-300 ring, white/10 in dark mode), ghost (text only, gray hover). They are disabled with the field, and clicking one emits actionClick with { actionId, value } (never while disabled).

Register it on the token to restyle every <smart-textarea>:

Because TextareaComponent renders injected components via NgComponentOutlet (which passes inputs by canonical name), TextareaPresetComponent overrides cssClass as input<string>('') without the class alias. Bind it as [cssClass] when using the <smart-textarea-preset> selector directly, or just pass class on <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.