Skip to content

PhoneInput

A core InputController attached to a DOM <input>. It wires the field’s events, writes each new value and selection back, and emits a PhoneInputState snapshot to subscribers. No emit fires at construction. The initial state goes straight to the DOM; read it with getState.

function createPhoneInput(options: PhoneInputOptions): PhoneInput;

Attaches to an <input type="text"> or <input type="tel">; any other type throws. A second attach to the same element throws. Before the engine is loaded it throws EngineNotReadyError; see Load the engine. destroy releases the element.

const phone = createPhoneInput({
mode: 'national',
defaultRegion: 'US',
input: document.querySelector('input')!,
});
phone.getState().placeholder; // '(201) 555-0123'

A discriminated union on mode, exported as NationalPhoneInputOptions and InternationalPhoneInputOptions. 'national' extends NationalInputControllerConfig; 'international' extends InternationalInputControllerConfig. The adapter adds these fields:

Field Type Meaning
input PhoneInputElement Required. The <input> to attach to, type="text" or type="tel".
regionFilter readonly RegionCode[] | null Region filter applied at construction.
numberTypeFilter readonly NumberType[] | null Number-type filter applied at construction.
placeholderNumberType NumberType The type the placeholder demonstrates; 'MOBILE' by default.

A value already present in the input element seeds the controller when initialValue is omitted, which keeps server-rendered and pre-attach autofilled values.

type PhoneInputState = {
readonly value: string;
readonly region: RegionCode | null;
readonly selectionStart: number;
readonly selectionEnd: number;
readonly regionFilter: readonly RegionCode[] | null;
readonly numberTypeFilter: readonly NumberType[] | null;
readonly placeholder: string | null;
readonly validationError: ValidationError | null;
};

value, region, and the selection mirror the core InputState. The two filters echo the active restrictions.

placeholder is the example number for the resolved region, in the field’s own display shape. While the value resolves no region, it falls back to the configured defaultRegion. It is null without a region to fall back to, or when the region’s metadata carries no example for the configured type. A national US field gets '(201) 555-0123'. An international field with the calling code inside gets '1 201-555-0123'.

It lives in the state only. The DOM placeholder attribute stays untouched; you render it yourself.

validationError is getValidationError for the current value. An empty national field reports TOO_SHORT under its region.

  • Typing, including IME composition (insertText, insertReplacementText, compositionend)
  • Paste, drop, yank (insertFromPaste, insertFromDrop, insertFromYank)
  • Deletes, including cut and drag-out (deleteContentBackward, deleteContentForward, deleteByCut, deleteByDrag, deleteContent)
  • Word deletes (deleteWordBackward, deleteWordForward): each consumes one digit group, skipping adjacent formatting
  • Line deletes (deleteSoftLineBackward, deleteHardLineForward, deleteEntireSoftLine, and the rest of the family)
  • Undo and redo: the native historyUndo and historyRedo input types, plus Cmd/Ctrl+Z, Cmd/Ctrl+Shift+Z, and Cmd/Ctrl+Y
  • Browser autofill and password managers: a value change that arrives with a bare input event resynchronizes the controller from the field
subscribe(listener: PhoneInputListener): () => void;

Subscribes to state changes. Returns an unsubscribe function. PhoneInputListener is (state: PhoneInputState) => void.

getState(): PhoneInputState;

The current state, without subscribing.

setValue(value: string): void;

Replaces the value through the controller’s setValue, writes the result back, then emits. Browser autofill and password managers announce their writes with an input event, and the widget adopts those on its own. A script that assigns input.value directly fires no event; push such values through this method.

setRegion(region: RegionCode): void;

Switches the region through the controller’s setRegion, then emits.

setRegionFilter(regions: readonly RegionCode[] | null): void;

Applies the region filter, then emits. An equal filter value is a no-op.

setNumberTypeFilter(numberTypes: readonly NumberType[] | null): void;

Applies the number-type filter, then emits. An equal filter value is a no-op.

undo(): void;

Steps the controller history back, then emits. With nothing to undo, it is a no-op.

redo(): void;

Steps the history forward again, then emits. With nothing to redo, it is a no-op.

canUndo(): boolean;

Whether an undo step exists.

canRedo(): boolean;

Whether a redo step exists.

clearHistory(): void;

Drops the undo and redo history, then emits.

getPhoneNumber(): PhoneNumber;

The current value as a PhoneNumber to query.

phone.getPhoneNumber().formatE164(); // '+14155550132'
destroy(): void;

Detaches the DOM listeners, releases the element, and clears subscribers. Idempotent. After it, the field behaves as a plain input again.

A RegionList supplies the rows; the picker’s selection hands its region to setRegion. The full wiring, with a popup and a live demo, is in Build the complete field.