Frameworks
@telixon/web-sdk is headless and framework-free: createPhoneInput attaches to a DOM <input>
element and drives it directly. Inside a component framework the integration is the same three
steps everywhere.
- Create after the engine is ready and the element is in the DOM.
createPhoneInputthrowsEngineNotReadyErrorwhen the engine has not finished loading. Components may each callensureEngineReady(): the engine is a process-wide singleton and concurrent calls share one load. - Subscribe into framework state. Every change emits a fresh, immutable
PhoneInputStatesnapshot. The initial state is applied to the DOM at construction but not emitted; readgetState()once after creating to seed your state. - Destroy on teardown.
destroy()detaches the DOM listeners, clears subscribers, and releases the element for a future attach.
Leave the input uncontrolled. The adapter writes the value and the caret to the element itself;
binding the framework’s own value mechanism (React value, Angular forms directives, Vue
v-model) to the same element would overwrite those writes. Render from the subscribed state
instead.
Component wiring
Section titled “Component wiring”The examples share one configuration, { mode: 'international', input }, so only the lifecycle
wiring differs between tabs.
import { afterNextRender, ChangeDetectionStrategy, Component, DestroyRef, ElementRef, inject, signal, viewChild,} from '@angular/core';import { ensureEngineReady } from '@telixon/core';import { createPhoneInput, type PhoneInput, type PhoneInputState } from '@telixon/web-sdk';
@Component({ selector: 'app-phone-field', changeDetection: ChangeDetectionStrategy.OnPush, template: ` <input #phone type="tel" autocomplete="tel" /> @if (phoneState()?.validationError; as validationError) { <p role="alert">{{ validationError.kind }}</p> } `,})export class PhoneFieldComponent { private readonly inputRef = viewChild.required<ElementRef<HTMLInputElement>>('phone'); private readonly destroyRef = inject(DestroyRef);
private phoneInput: PhoneInput | null = null; private isDestroyed = false;
protected readonly phoneState = signal<PhoneInputState | null>(null);
constructor() { afterNextRender(() => void this.attach()); this.destroyRef.onDestroy(() => { this.isDestroyed = true; this.phoneInput?.destroy(); }); }
private async attach(): Promise<void> { await ensureEngineReady(); if (this.isDestroyed) return;
this.phoneInput = createPhoneInput({ mode: 'international', input: this.inputRef().nativeElement, }); this.phoneState.set(this.phoneInput.getState()); this.phoneInput.subscribe((nextState) => this.phoneState.set(nextState)); }}afterNextRender runs only in the browser and only after the view is attached, so it settles
both preconditions in one place: the element exists, and under server-side rendering the
controller is never created on the server. The isDestroyed guard handles a component destroyed
while ensureEngineReady() is still in flight. Signal writes from the subscription notify change
detection in both zone-based and zoneless applications.
import { useEffect, useRef, useState } from 'react';import { ensureEngineReady } from '@telixon/core';import { createPhoneInput, type PhoneInput, type PhoneInputState } from '@telixon/web-sdk';
export function PhoneField() { const inputRef = useRef<HTMLInputElement>(null); const [phoneState, setPhoneState] = useState<PhoneInputState | null>(null);
useEffect(() => { const input = inputRef.current; if (input === null) return;
let phoneInput: PhoneInput | null = null; let cancelled = false;
void ensureEngineReady().then(() => { if (cancelled) return; phoneInput = createPhoneInput({ mode: 'international', input }); setPhoneState(phoneInput.getState()); phoneInput.subscribe(setPhoneState); });
return () => { cancelled = true; phoneInput?.destroy(); }; }, []);
return ( <> <input ref={inputRef} type="tel" autoComplete="tel" /> {phoneState?.validationError && <p role="alert">{phoneState.validationError.kind}</p>} </> );}StrictMode. In development, StrictMode runs the effect, its cleanup, and the effect again.
The example handles both timings of that cycle. When the engine resolved before cleanup, the
cleanup destroys the first controller, and destroy() releases the element for the second run;
createPhoneInput throws on an element that already has an attached phone input, so this release
is what makes the remount legal. When the engine promise is still pending at cleanup, the
cancelled flag stops the first run from attaching at all.
Server rendering. Effects never run on the server. The component server-renders as a plain input; the engine downloads and the controller attaches in the browser.
<script setup lang="ts">import { onBeforeUnmount, onMounted, shallowRef, useTemplateRef } from 'vue';import { ensureEngineReady } from '@telixon/core';import { createPhoneInput, type PhoneInput, type PhoneInputState } from '@telixon/web-sdk';
const inputRef = useTemplateRef<HTMLInputElement>('phone');const phoneState = shallowRef<PhoneInputState | null>(null);
let phoneInput: PhoneInput | null = null;let isUnmounted = false;
onMounted(async () => { await ensureEngineReady(); if (isUnmounted || inputRef.value === null) return;
phoneInput = createPhoneInput({ mode: 'international', input: inputRef.value }); phoneState.value = phoneInput.getState(); phoneInput.subscribe((nextState) => { phoneState.value = nextState; });});
onBeforeUnmount(() => { isUnmounted = true; phoneInput?.destroy();});</script>
<template> <input ref="phone" type="tel" autocomplete="tel" /> <p v-if="phoneState?.validationError" role="alert">{{ phoneState.validationError.kind }}</p></template>shallowRef matches how the adapter emits: every emit is a new immutable snapshot, so replacing
the reference is the whole update, and deep reactivity would proxy an object that never mutates.
The
isUnmounted guard handles a component unmounted while ensureEngineReady() is still in flight.
Under server-side rendering onMounted runs only in the browser, so the controller is never
created on the server.
Next steps
Section titled “Next steps”- Engine loading covers where to call
ensureEngineReady(), at application bootstrap or at first use. - Validation feedback shows how to render
state.validationErroras form feedback. - DOM adapter explains what the adapter does to the element, and why it must own the value.
- The web-sdk reference lists every option and method on
PhoneInput.