Skip to content

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.

  1. Create after the engine is ready and the element is in the DOM. createPhoneInput throws EngineNotReadyError when the engine has not finished loading. Components may each call ensureEngineReady(): the engine is a process-wide singleton and concurrent calls share one load.
  2. Subscribe into framework state. Every change emits a fresh, immutable PhoneInputState snapshot. The initial state is applied to the DOM at construction but not emitted; read getState() once after creating to seed your state.
  3. 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.

The examples share one configuration, { mode: 'international', input }, so only the lifecycle wiring differs between tabs.

phone-field.component.ts
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.

  • Engine loading covers where to call ensureEngineReady(), at application bootstrap or at first use.
  • Validation feedback shows how to render state.validationError as 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.