Skip to content

Build a phone input

A phone input pairs a plain text field with an InputController. The field emits edits; the controller returns the next state to write back.

A single-region form takes a national controller. For a field that accepts any region, jump to the international field.

import { createNationalInputController } from '@telixon/core';
const controller = createNationalInputController({ defaultRegion: 'US' });

Translate the field’s beforeinput events into controller calls. Write each returned state back:

import type { InputState } from '@telixon/core';
const input = document.querySelector('input')!;
function apply(state: InputState) {
input.value = state.value;
input.setSelectionRange(state.selectionStart, state.selectionEnd);
}
input.addEventListener('beforeinput', (event) => {
event.preventDefault();
const value = input.value;
const start = input.selectionStart ?? 0;
const end = input.selectionEnd ?? 0;
if (event.inputType === 'deleteContentBackward') {
apply(controller.deleteBackward(value, start, end));
} else if (event.inputType === 'deleteContentForward') {
apply(controller.deleteForward(value, start, end));
} else {
const text = event.data ?? event.dataTransfer?.getData('text') ?? '';
apply(controller.insert(value, text, start, end));
}
});

Typing 4155550132 leaves the field showing (415) 555-0132, with the caret after the last digit. A paste flows through the same handler. For a production binding, use @telixon/web-sdk, the DOM adapter for controllers.

The controller already holds the history; wire the keys to it:

input.addEventListener('keydown', (event) => {
if (!(event.metaKey || event.ctrlKey) || event.key.toLowerCase() !== 'z') return;
event.preventDefault();
apply(event.shiftKey ? controller.redo() : controller.undo());
});

getPhoneNumber answers for the current value at any moment, mid-typing included:

controller.insert('', '4155550132', 0, 0);
controller.getPhoneNumber().isValid(); // true
controller.getPhoneNumber().formatE164(); // '+14155550132'

While the value is unfinished, prefer the tolerant checks; see Validate user input.

A field that takes numbers from anywhere switches to the international controller. The region follows the digits from the first keystroke:

import { createInternationalInputController } from '@telixon/core';
const controller = createInternationalInputController();
let state = controller.insert('', '1', 0, 0);
// { value: '1 ', region: 'US', selectionStart: 2, selectionEnd: 2 }
state = controller.insert(state.value, '4155550132', 2, 2);
// { value: '1 415-555-0132', region: 'US', selectionStart: 14, selectionEnd: 14 }

The display modes, including the leading +, are under InternationalInputControllerConfig.

A field for one kind of number filters the rest out. A support-line field takes only toll-free numbers:

const supportLine = createNationalInputController({ defaultRegion: 'US' });
supportLine.setNumberTypeFilter(['TOLL_FREE']);
supportLine.setValue('8002345678');
supportLine.getPhoneNumber().isValid(); // true
supportLine.setValue('4155550132');
supportLine.getPhoneNumber().isValid(); // false