Skip to content

createInternationalInputController

createInternationalInputController creates an InputController whose value carries the calling code, so the region is inferred from what the user types instead of being fixed up front. Digits drive one deterministic automaton compiled from Google’s metadata; parsePhoneNumber and the input controller are the same shared resolve layer, queried in batch versus per keystroke.

This page documents construction and the behavior specific to international input. The returned controller’s methods, InputState fields, and history semantics are defined once on the InputController contract page.

import { createInternationalInputController } from '@telixon/core';
import type { InternationalDisplayConfig, InternationalInputControllerConfig, PlusPrefixMode } from '@telixon/core';
function createInternationalInputController(config: InternationalInputControllerConfig = {}): InputController;

config is optional and defaults to {}.

// 'none' renders no plus, 'fixed' renders a permanent plus, 'erasable' renders a plus the user can delete and retype.
export type PlusPrefixMode = 'none' | 'fixed' | 'erasable';
export type InternationalDisplayConfig =
| { readonly callingCodeInInput: false }
| { readonly callingCodeInInput: true; readonly plusPrefix: PlusPrefixMode };
export type InternationalInputControllerConfig =
| {
defaultRegion?: RegionCode;
strict?: boolean;
initialValue?: string;
maxHistorySize?: number;
display?: Extract<InternationalDisplayConfig, { callingCodeInInput: true }>;
}
| {
defaultRegion: RegionCode;
strict?: boolean;
initialValue?: string;
maxHistorySize?: number;
display: Extract<InternationalDisplayConfig, { callingCodeInInput: false }>;
};

The config is a discriminated union on display.callingCodeInInput. With callingCodeInInput: true (the default) the calling code lives inside the input value and plusPrefix chooses how the leading + renders. With callingCodeInInput: false the value holds only national digits, so defaultRegion becomes required; the union makes the compiler enforce this.

Field Type Default Meaning
display InternationalDisplayConfig { callingCodeInInput: true, plusPrefix: 'none' } Whether the calling code is part of the input value, and how the leading + renders.
defaultRegion RegionCode none Region that supplies the calling code. Optional when the calling code is in the input; required when it is not.
strict boolean false Resolves only within defaultRegion. See Enforce strict mode.
initialValue string '', or the defaultRegion calling code when the calling code is in the input Value the controller starts from. The starting state is the history root, so canUndo is false.
maxHistorySize number 150 Maximum number of undo history entries.

Each PlusPrefixMode in one line:

  • 'none': no plus is rendered; the value starts with the calling code digits.
  • 'fixed': the plus is always shown and cannot be edited away.
  • 'erasable': the plus is shown, deletable with backspace or forward delete at position 0, and restored by typing +.

An InputController, the same contract createNationalInputController returns. getPhoneNumber() resolves the current value to a PhoneNumber.

import { createInternationalInputController, ensureEngineReady } from '@telixon/core';
await ensureEngineReady();
const controller = createInternationalInputController({
display: { callingCodeInInput: true, plusPrefix: 'fixed' },
});
controller.setValue('+442079460958');
controller.currentState.value; // '+44 20 7946 0958'
controller.currentState.region; // 'GB'
controller.getPhoneNumber().formatE164(); // '+442079460958'

The factory throws EngineNotReadyError when called before the engine is initialized. Initialization is explicit; see ensureEngineReady.

import { createInternationalInputController, EngineNotReadyError } from '@telixon/core';
try {
createInternationalInputController();
} catch (error) {
error instanceof EngineNotReadyError; // true
}

After construction, invalid input does not throw. Validity is an accepting state, and every ValidationError variant is a named way of not being in one: an empty value resolves to a PhoneNumber whose getValidationError() returns { kind: 'EMPTY' }.

Every keystroke feeds digits to the automaton, which consumes the calling code before the national number. While the calling code is partial, region reports the primary region of the current automaton state and can change with the next digit. Once the calling code completes, the value gains a separator space and the national digits format progressively in the inferred region’s international layout; when they complete a format pattern, the value matches formatInternational().

National digits keep narrowing the region across shared calling codes:

const controller = createInternationalInputController({
defaultRegion: 'CA',
display: { callingCodeInInput: true, plusPrefix: 'fixed' },
});
controller.setValue('+12125550198');
controller.currentState.region; // 'US', not 'CA': the national digits are a New York number

With callingCodeInInput: true, digits are always resolved as calling code first, whatever the plus mode. Hiding the plus ('none') or erasing it ('erasable') never changes resolution:

const controller = createInternationalInputController(); // plusPrefix 'none'
controller.setValue('+442079460958');
controller.currentState.value; // '44 20 7946 0958', 'none' renders no plus
controller.getPhoneNumber().formatE164(); // '+442079460958'

With plusPrefix: 'erasable', setValue reads plus visibility from the string: a value starting with + shows the plus, a non-empty value without one starts with the plus erased, and '' resets to the visible plus. With 'fixed' the plus is always rendered; with 'none' it never is.

With display: { callingCodeInInput: false } the value holds only national digits and the required defaultRegion supplies the calling code. setRegion re-seeds the calling code and re-resolves the digits already typed. getPhoneNumber() still resolves the full international number.

With the calling code in the input, defaultRegion seeds the initial value with the region’s calling code, so the user starts typing after it. An explicit initialValue overrides the seed.

const controller = createInternationalInputController({ defaultRegion: 'US' });
controller.currentState.value; // '1 '
controller.currentState.region; // 'US'

With strict: true, resolution never leaves defaultRegion: the reported region stays defaultRegion, and numbers that only match elsewhere are invalid. Full semantics are in Enforce strict mode.

const controller = createInternationalInputController({
defaultRegion: 'CA',
strict: true,
display: { callingCodeInInput: true, plusPrefix: 'fixed' },
});
controller.setValue('+12125550198');
controller.currentState.value; // '+1 212-555-0198'
controller.currentState.region; // 'CA'
controller.getPhoneNumber().isValid(); // false: a US number rejected under CA

Each insert(value, text, selectionStart, selectionEnd) call mirrors one keystroke; the controller returns the next value, caret, and region.

const controller = createInternationalInputController({
display: { callingCodeInInput: true, plusPrefix: 'fixed' },
});
controller.currentState.value; // '+'
let state = controller.currentState;
for (const digit of ['4', '4', '2', '0', '7', '9', '4', '6', '0', '9']) {
state = controller.insert(state.value, digit, state.selectionStart, state.selectionEnd);
}
// value after each keystroke:
// '+4' region 'GB'
// '+44 ' region 'GB', calling code complete
// '+44 2'
// '+44 20 '
// '+44 20 7'
// '+44 20 79'
// '+44 20 794'
// '+44 20 7946 '
// '+44 20 7946 0'
// '+44 20 7946 09'
state.region; // 'GB'
const controller = createInternationalInputController({
display: { callingCodeInInput: true, plusPrefix: 'erasable' },
});
controller.currentState.value; // '+'
controller.setValue('+44 20 7946 0958');
// Backspace with the caret right after the plus erases only the plus.
controller.deleteBackward('+44 20 7946 0958', 1, 1);
controller.currentState.value; // '44 20 7946 0958'
// Typing '+' at position 0 restores it.
controller.insert('44 20 7946 0958', '+', 0, 0);
controller.currentState.value; // '+44 20 7946 0958'
// setValue infers plus visibility from the string it receives.
controller.setValue('442079460958');
controller.currentState.value; // '44 20 7946 0958'
controller.getPhoneNumber().formatE164(); // '+442079460958'
controller.setValue('');
controller.currentState.value; // '+'

The value stays national while the resolved number stays international. Pair this mode with a separate region control that calls setRegion.

const controller = createInternationalInputController({
defaultRegion: 'GB',
display: { callingCodeInInput: false },
});
controller.setValue('2079460958');
controller.currentState.value; // '20 7946 0958'
controller.currentState.region; // 'GB'
controller.getPhoneNumber().formatE164(); // '+442079460958'
// setRegion re-seeds the calling code and re-resolves the same digits.
controller.setValue('2125550198');
controller.currentState.value; // '21 2555 0198'
controller.setRegion('US');
controller.currentState.value; // '212-555-0198'
controller.getPhoneNumber().formatE164(); // '+12125550198'