Input Validation
One editor, three form systems. Validation messages resolve from the i18n config (provideOgeInputsConfig), show per the errorDisplay policy (default: after first blur) and are announced via aria-live. Signal Forms schema constraints (required, minLength, max…) auto-bind into the editors through the FormValueControl contract.
Standalone validation
No forms library required: drive the error state yourself with the invalid flag and an explicit errorText message. errorDisplay chooses when errors surface — after the first blur (touched, the default), after the first edit (dirty), or immediately (always).
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeTextBox } from '@oge-ui/inputs';
@Component({
selector: 'demo-root',
imports: [OgeTextBox],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- no forms library: drive state via inputs -->
<oge-text-box
label="Username"
[(value)]="username"
[invalid]="username().length > 0 && username().length < 3"
errorText="At least 3 characters"
errorDisplay="always"
/>
`,
})
export class Demo {
protected readonly username = signal('');
}Reactive Forms
Bind with formControl/formControlName as usual — the editor renders the control's validation errors in its own subscript, localized through the messages config (required, email, minlength, min/max…). Touched state, markAllAsTouched(), disable/enable and form resets all flow through automatically.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ReactiveFormsModule, FormControl, Validators } from '@angular/forms';
import { OgeTextBox } from '@oge-ui/inputs';
@Component({
selector: 'demo-root',
imports: [ReactiveFormsModule, OgeTextBox],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-text-box label="E-mail" mode="email" [formControl]="email" hint="required + email" />
`,
})
export class Demo {
// classic reactive forms — the CVA bridge renders control errors
protected readonly email = new FormControl('', {
nonNullable: true,
validators: [Validators.required, Validators.email],
});
}Signal Forms
The editors implement Angular's FormValueControl contract, so [formField] binds them natively: schema rules like required(), minLength() and max() push their errors and their constraints (native attributes, number clamping bounds) straight into the editor. Blur emits the contract's touch, driving the field's touched state.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { FormField, form, minLength, required } from '@angular/forms/signals';
import { OgeNumberBox, OgeTextBox } from '@oge-ui/inputs';
@Component({
selector: 'demo-root',
imports: [FormField, OgeNumberBox, OgeTextBox],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-text-box label="Username" [formField]="f.username" />
<oge-number-box label="Age" [formField]="f.age" />
`,
})
export class Demo {
// Angular Signal Forms — schema constraints auto-bind
protected readonly model = signal({ username: '', age: null as number | null });
protected readonly f = form(this.model, (p) => {
required(p.username);
minLength(p.username, 3);
});
}Linked fields
Cross-field rules need no event wiring: bind one field's state to another's signal ([disabled], [min]…) and the relationship stays live. When you do want an imperative hook, valueCommitted delivers value, previousValue and the originating DOM event — undefined event means the change was programmatic, not typed.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeButton, OgeButtonGroup } from '@oge-ui/buttons';
import { OgeNumberBox, OgeTextBox } from '@oge-ui/inputs';
import type { OgeInputValueCommittedEvent } from '@oge-ui/inputs';
@Component({
selector: 'demo-root',
imports: [OgeButton, OgeButtonGroup, OgeNumberBox, OgeTextBox],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- cross-field rules: bind state to state — no callbacks needed -->
<oge-button-group selectionMode="single" [(selectedKeys)]="invoiceType">
<oge-button value="person" text="Person" />
<oge-button value="company" text="Company" />
</oge-button-group>
<oge-text-box label="Tax ID" [disabled]="!invoiceType().includes('company')" />
<!-- Max takes its lower bound from Min -->
<oge-number-box label="Min" [(value)]="minValue" />
<oge-number-box
label="Max"
[min]="minValue() ?? undefined"
(valueCommitted)="onMaxChanged($event)"
/>
`,
})
export class Demo {
protected readonly invoiceType = signal<string[]>(['person']);
protected readonly minValue = signal<number | null>(null);
// rich change payload: { value, previousValue, event }
protected onMaxChanged(e: OgeInputValueCommittedEvent<number | null>): void {
console.log(e.previousValue, '→', e.value, e.event ? 'user' : 'programmatic');
}
}Async validation indicator
While a server-side check runs, set pending and a spinner appears in the suffix rail (with screen-reader text). Pair it with showSuccessIcon to confirm a passing value — the success mark hides automatically whenever the field is empty, invalid or pending.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeTextBox } from '@oge-ui/inputs';
@Component({
selector: 'demo-root',
imports: [OgeTextBox],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-text-box
label="API key"
[(value)]="apiKey"
[pending]="checking()"
[showSuccessIcon]="'always'"
hint="type to trigger a fake async check"
(inputChange)="simulateCheck()"
/>
`,
})
export class Demo {
protected readonly apiKey = signal('');
protected readonly checking = signal(false);
private checkTimer: ReturnType<typeof setTimeout> | undefined;
// `pending` shows a rail spinner; pair it with async validation
protected simulateCheck(): void {
this.checking.set(true);
clearTimeout(this.checkTimer);
this.checkTimer = setTimeout(() => this.checking.set(false), 900);
}
}