Skip to content

Build a phone input

A phone input pairs a plain <input> 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:

<input id="phone" type="tel" autocomplete="tel" />
import type { InputState } from '@telixon/core';
const input = document.querySelector<HTMLInputElement>('#phone')!;
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. This minimal handler stops at typing, paste, and plain deletes; word deletes, line deletes, and IME composition need branches of their own, which PhoneInput from @telixon/web-sdk already wires for production.

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

input.addEventListener('keydown', (event) => {
if (!event.metaKey && !event.ctrlKey) return;
const key = event.key.toLowerCase();
if (key !== 'z' && key !== 'y') return;
event.preventDefault();
const isRedo = (key === 'z' && event.shiftKey) || key === 'y';
apply(isRedo ? 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'

The full query surface is available on that number, including the possibility check and the typed validation error; see Validate a phone number.

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