Current version 22.5.0

Input Beta

import { Tg2InputContainerComponent, Tg2InputDirective } from '@trademe/tangram';

Write a native <input> or <textarea> with the tg2Input attribute inside <tg2-input-container>. Form binding (ngModel, formControlName, formField) and native attributes (type, placeholder, maxlength, readonly, autocomplete) go on the native element, not the container.


Form systems

The container has no value input or output — every form system Angular supports works the same way it always has, straight on the projected <input>.

ngModel

<tg2-input-container label="Title">
    <input tg2Input required [(ngModel)]="title" />
</tg2-input-container>

Reactive forms

readonly form = new FormGroup({
    email: new FormControl('', { nonNullable: true, validators: [Validators.required, Validators.email] })
});
<form [formGroup]="form">
    <tg2-input-container label="Email">
        <input tg2Input type="email" formControlName="email" />
    </tg2-input-container>
</form>

Signal Forms

import { FormField, form, required } from '@angular/forms/signals';

readonly model = signal({ username: '' });
readonly userForm = form(this.model, path => {
    required(path.username, { message: 'Username is required' });
});
<tg2-input-container label="Username">
    <input tg2Input [formField]="userForm.username" />
</tg2-input-container>

No forms library

A plain [value] / (input) pair works too — the container reads state straight off the native element.

<tg2-input-container label="Title">
    <input tg2Input required [value]="title" (input)="title = $any($event.target).value" />
</tg2-input-container>

States

Input states
Large (default)Medium
Default
Filled
Disabled
Read-only
Error

Label

"(optional)" is added to the label of any field that isn't required. Mark a field required where your form expects it (the required attribute, Validators.required, any validator that returns { required: true } for an empty value, or a Signal Forms required() rule) and the container follows. Set showOptional="false" to hide it on a field that isn't really a form question, such as a search box.

Angular doesn't emit an event when validators change. After setValidators(), addValidators(), removeValidators() or clearValidators(), call updateValueAndValidity() so "(optional)" and aria-required update. The control's own errors need the same call.

Label options
DefaultHiddenOptionalFloating

Icons and clear button

Icons and clear button
Icon leftIcon rightClearable

Characters remaining and textarea

Characters remaining and textarea
Characters remainingOver the limitTextarea

A native maxlength stops the user typing at the limit. To let them type past it, set characterLimit on the container instead. Over the limit, the counter shows how many characters too many there are, in the error style.

Going over the limit is a warning while the user types, not an error: the border, aria-invalid and the form's validity don't change. To stop the form being sent, pair it with your own validation. With reactive forms or Signal Forms, add a max length validator and an errorMessage: once the field is touched and too long, the error replaces the counter. With ngModel, use [error] instead, because Angular's maxlength validator also sets the native attribute, which stops typing.

Screen readers hear the count after the user pauses typing, through a hidden live region, so it isn't read out on every keystroke.

readonly message = new FormControl('', { validators: [Validators.required, Validators.maxLength(500)] });

<tg2-input-container
    label="Message"
    characterLimit="500"
    showRemainingCharacters
    errorMessage="Keep your message to 500 characters">
    <textarea tg2Input [formControl]="message"></textarea>
</tg2-input-container>

Errors from your own validation

By default the error shows once the Angular control is touched and invalid. If your errors are worked out somewhere else, for example in signals with no validators on the control, set [error]. true shows the error (and sets aria-invalid), false hides it, and leaving it unset goes back to the control's state. It works without a forms library too.

readonly firstName = signal('');
readonly firstNameError = computed(() => (this.touched() || this.submitted()) && !this.firstName());

<tg2-input-container label="First name" errorMessage="Enter your first name" [error]="firstNameError()">
    <input tg2Input required [ngModel]="firstName()" (ngModelChange)="firstName.set($event)" />
</tg2-input-container>

Radius

Set radius to small (4px), medium (8px) or large (16px). Left unset, the theme decides: 8px, or 3px in the tg-light and tg-dark themes.

Radius
small (4px)medium (8px)large (16px)

API

tg2-input-container

The container has no value input or output — bind your form directive on the native element instead. aria-invalid, aria-required and aria-describedby are wired automatically. Required is picked up from the native required attribute or Validators.required, so reactive forms don't need required on the element as well. Only one footer message shows at a time, in this priority order: error, then remaining characters, then description.

tg2-input-container inputs
InputTypeDefaultDescription
label (required)string—The field's label text.
showLabelbooleantrueVisually hides the label. Screen readers still announce it.
descriptionstring—Helper text shown in the footer when there is no error or character count to show.
errorMessagestring— Shown in the footer once the control is touched and invalid, or when error is true.
errorboolean— Sets the error state directly, for validation outside the Angular forms API. Unset follows the control.
showOptionalbooleantrue "(optional)" is appended to the label automatically when the field isn't required. Set false to hide it.
floatingLabelbooleanfalsePuts the label inside the field. Ignored when showLabel is false.
sizeTg2InputSizeEnum (medium | large)largeField height. Medium is 38px. Large is 48px, or 56px with a floating label.
iconLeftTangramIconKind—Decorative icon shown before the field.
iconRightTangramIconKind—Decorative icon shown after the field.
clearablebooleanfalse Shows a clear button while the field has a value. Clears the native value and dispatches an input event; does not submit the form.
showRemainingCharactersbooleanfalse Needs characterLimit or a maxlength on the native element.
characterLimitnumber— Limit for the characters remaining counter. Unlike maxlength it doesn't stop typing, so the counter can show how far over the value is. Over the limit is a warning, not an error: pair it with a max length validator and errorMessage to block sending. Unset uses the native maxlength.
radiussmall | medium | large—Corner radius (4px, 8px, 16px). Unset follows the theme: 8px, or 3px for tg-light and tg-dark.
themetg2-light | tg2-dark | tg-light | tg-darktg2-lightWhich visual design system the field uses.

tg2Input

Selector: input[tg2Input], textarea[tg2Input].

tg2Input directive inputs
InputTypeDescription
idstringA consumer-supplied id for the native element. When omitted, an id is generated automatically.
aria-describedbystring Ids of your own elements that describe the field. They're kept, and the footer's id is added after them. Set it as aria-describedby="…" or [aria-describedby]="…", not [attr.aria-describedby], which would overwrite the footer's id.

Try every option interactively on the Code Example page.

Proudly made by © 2026 Trade Me Limited