Masked Text Box
A mask-first text box — the Kendo / Syncfusion MaskedTextBox, DevExtreme's mask option. Typing fills the slot at the caret and skips literals, Backspace and Delete empty a slot without shifting the rest, paste takes raw or formatted text, and IME composition is applied once it settles. Positions are logical, so the editor behaves the same in RTL.
The engine is @oge-ui/behavior's OgeMaskCore, and the same mask input works on <oge-text-box>. It binds through [(value)], [formField] or formControl like every other editor in the family.
Getting started
0 is a required digit; the parentheses, the space and the dash are literals the caret skips. The value is the raw digits — no literals, no placeholders — and a digit-only mask asks mobile keyboards for the numeric layout (inputmode="numeric").
Value:
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeMaskedTextBox } from '@oge-ui/inputs/masked-text-box';
@Component({
selector: 'demo-root',
imports: [OgeMaskedTextBox],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- 0 = a required digit; ( ) - and the space are literals the caret
skips. The value is the raw digits — no literals, no placeholders. -->
<oge-masked-text-box
label="Phone"
mask="(000) 000-0000"
[(value)]="phone"
autocomplete="tel-national"
/>
<p>Value: <code>{{ phone() }}</code></p>
`,
})
export class Demo {
protected readonly phone = signal('');
}Mask syntax and custom rules
0 digit, 9 optional digit, # digit/space/sign, L/l letter (required/optional, any script), A/a letter or digit, C/c any character; a backslash makes a rule character literal. maskRules adds single-character keys — a RegExp, a string of allowed characters or a predicate.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeInputPrefix } from '@oge-ui/inputs';
import { OgeMaskedTextBox } from '@oge-ui/inputs/masked-text-box';
import type { OgeMaskRules } from '@oge-ui/inputs';
@Component({
selector: 'demo-root',
imports: [OgeInputPrefix, OgeMaskedTextBox],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- L = a required letter (any script), 0 = a digit -->
<oge-masked-text-box label="Licence plate" mask="00 LLL 000" [(value)]="plate" />
<!-- a custom rule: H accepts one hex digit (always a required slot) -->
<oge-masked-text-box label="Hex color" mask="HHHHHH" [maskRules]="hexRules" [(value)]="hex">
<span ogeInputPrefix>#</span>
</oge-masked-text-box>
<!-- 9 = an optional digit; "ext. " has no rule characters, so it is literal -->
<oge-masked-text-box label="Extension" mask="ext. 0999" [(value)]="extension" />
<!-- a backslash makes a rule character literal: "A" here is printed, not typed -->
<oge-masked-text-box label="Product code" [mask]="productMask" [(value)]="product" />
`,
})
export class Demo {
protected readonly plate = signal('');
protected readonly hex = signal('');
protected readonly extension = signal('');
protected readonly product = signal('');
protected readonly hexRules: OgeMaskRules = { H: /[0-9a-f]/i };
protected readonly productMask = '\\A-000-LL';
}Raw and formatted values
By default the value is the raw characters (DevExtreme's useMaskedValue: false, Kendo's raw value); includeLiterals commits the formatted text instead. maskCompleted fires once, when the last required slot is filled.
raw: · formatted:
Type all 16 digits
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeMaskedTextBox } from '@oge-ui/inputs/masked-text-box';
import type { OgeMaskCompletedEvent } from '@oge-ui/inputs';
@Component({
selector: 'demo-root',
imports: [OgeMaskedTextBox],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- raw value (default) vs the formatted text with includeLiterals -->
<oge-masked-text-box label="Card (raw)" mask="0000 0000 0000 0000" [(value)]="raw" />
<oge-masked-text-box
label="Card (with literals)"
mask="0000 0000 0000 0000"
[includeLiterals]="true"
[(value)]="formatted"
(maskCompleted)="onCompleted($event)"
/>
<p>raw: <code>{{ raw() }}</code> · formatted: <code>{{ formatted() }}</code></p>
<p>{{ status() }}</p>
`,
})
export class Demo {
protected readonly raw = signal('');
protected readonly formatted = signal('');
protected readonly status = signal('Type all 16 digits');
// fires once, when the last required slot is filled
protected onCompleted(event: OgeMaskCompletedEvent): void {
this.status.set(`Complete: ${event.maskedValue}`);
}
}Mask display
maskChar sets the placeholder of empty slots. showMaskMode: 'onFocus' keeps the empty mask hidden while blurred so the placeholder shows; a floating label does the same, so the label never sits on top of the mask.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeMaskedTextBox } from '@oge-ui/inputs/masked-text-box';
@Component({
selector: 'demo-root',
imports: [OgeMaskedTextBox],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- a custom placeholder character -->
<oge-masked-text-box label="Date code" mask="00.00.0000" maskChar="•" [(value)]="code" />
<!-- the mask only appears on focus — the placeholder shows while blurred -->
<oge-masked-text-box
label="Postal code"
mask="00000"
showMaskMode="onFocus"
placeholder="5 digits"
[(value)]="postal"
/>
<!-- floating labels keep the empty mask hidden until the field is focused -->
<oge-masked-text-box label="PIN" labelMode="floating" mask="0000" [(value)]="pin" />
`,
})
export class Demo {
protected readonly code = signal('');
protected readonly postal = signal('');
protected readonly pin = signal('');
}Validation and forms
An unfinished mask flags the field (after blur, per errorDisplay) with maskInvalidMessage or the catalog's maskInvalidError. A reactive control gets a { mask } validator attached automatically; a Signal Forms schema checks the same rule with ogeMaskComplete(), which also runs on a server.
IBAN valid: false
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { ReactiveFormsModule, FormControl, Validators } from '@angular/forms';
import { FormField, form, required, validate } from '@angular/forms/signals';
import { OgeMaskedTextBox } from '@oge-ui/inputs/masked-text-box';
import { ogeMaskComplete } from '@oge-ui/behavior';
@Component({
selector: 'demo-root',
imports: [ReactiveFormsModule, FormField, OgeMaskedTextBox],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- Signal Forms: the schema checks the mask with ogeMaskComplete() -->
<oge-masked-text-box label="IBAN" mask="LL00 0000 0000 0000 0000 00" [formField]="f.iban" />
<!-- reactive forms: the editor adds a { mask } validator to the control itself -->
<oge-masked-text-box
label="Tax number"
mask="000-000-0000"
maskInvalidMessage="Enter all 10 digits"
[formControl]="taxNumber"
/>
`,
})
export class Demo {
protected readonly model = signal({ iban: '' });
protected readonly f = form(this.model, (p) => {
required(p.iban);
validate(p.iban, ({ value }) =>
ogeMaskComplete('LL00 0000 0000 0000 0000 00', value())
? undefined
: { kind: 'mask' },
);
});
protected readonly taxNumber = new FormControl('', {
nonNullable: true,
validators: [Validators.required],
});
}Mask on the text box
The same engine on the plain text box: mask is optional there, so a time field can keep its clear button, prefix/suffix slots and counter-free chrome. Without a mask it is the ordinary text box.
Value:
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeTextBox } from '@oge-ui/inputs';
@Component({
selector: 'demo-root',
imports: [OgeTextBox],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- the same engine on the plain text box: mask is optional there -->
<oge-text-box
label="Time"
mask="00:00"
[includeLiterals]="true"
[showClearButton]="true"
[(value)]="time"
/>
`,
})
export class Demo {
protected readonly time = signal('');
}