Skip to content

The elements in React, Vue, and Angular

The elements are custom elements, which every framework renders as markup. Each framework needs the call to defineTelixonElements() in its client entry. React and Vue take a type entry for their templates as well. The React and Vue sources on this page come from the package’s example.

@telixon/web-components/react types the three tags for JSX. The ref of a tag is typed as its element class, which hands the component the element’s properties. React 19 sets a prop as a property where the element has one, such as options, and listens to a custom event through its exact name, such as ontelixon-change. Both happen on a client render only. A server-rendered page passes the configuration as attributes and adds its listeners from an effect on the ref, which works on every page. The effect reads value once its listener is attached, since a field whose engine was cached may have gone live before the effect ran:

import type { TelixonPhoneChangeDetail, TelixonPhoneFieldElement } from '@telixon/web-components';
import type {} from '@telixon/web-components/react';
import { useEffect, useRef, useState } from 'react';
export function PhoneField() {
const field = useRef<TelixonPhoneFieldElement>(null);
const [value, setValue] = useState<string | null>(null);
const [submitted, setSubmitted] = useState('none');
// A hydrated page gets its listeners from an effect, since React 19 adds none while it hydrates.
useEffect(() => {
const element = field.current;
if (element === null) return;
const onChange = (event: CustomEvent<TelixonPhoneChangeDetail>): void => setValue(event.detail.value);
element.addEventListener('telixon-change', onChange);
// A field that went live before the effect ran holds its value already.
setValue(element.value);
return () => element.removeEventListener('telixon-change', onChange);
}, []);
return (
<form
id="contact"
onSubmit={(event) => {
event.preventDefault();
setSubmitted(String(new FormData(event.currentTarget).get('phone')));
}}
>
<label htmlFor="phone-input">Phone</label>
<telixon-phone-field
ref={field}
name="phone"
default-region="US"
calling-code-in-input="false"
auto-placeholder
required
>
<telixon-region-picker />
<input id="phone-input" type="tel" autoComplete="tel" />
</telixon-phone-field>
<button>Save</button>
<p>
Value: <output id="value">{value ?? 'null'}</output>
</p>
<p>
Submitted: <output id="submitted">{submitted}</output>
</p>
</form>
);
}

The server renders the same component through renderToString, as the server rendering guide shows.

@telixon/web-components/vue types the three tags for templates, where @telixon-change compiles to the onTelixonChange prop. One type import anywhere in the project, such as the entry, covers every template. Vue needs to know which tags are custom elements, through isCustomElement in the template compiler options. Vue sets :options and required as properties, since the element has both:

<script setup lang="ts">
import type { TelixonPhoneChangeDetail } from '@telixon/web-components';
import { ref } from 'vue';
const value = ref<string | null>(null);
const submitted = ref('none');
function onChange(event: CustomEvent<TelixonPhoneChangeDetail>): void {
value.value = event.detail.value;
}
function onSubmit(event: Event): void {
if (event.currentTarget instanceof HTMLFormElement) {
submitted.value = String(new FormData(event.currentTarget).get('phone'));
}
}
</script>
<template>
<form id="contact" @submit.prevent="onSubmit">
<label for="phone-input">Phone</label>
<telixon-phone-field
name="phone"
default-region="US"
calling-code-in-input="false"
auto-placeholder
required
@telixon-change="onChange"
>
<telixon-region-picker></telixon-region-picker>
<input id="phone-input" type="tel" autocomplete="tel" />
</telixon-phone-field>
<button>Save</button>
<p>
Value: <output id="value">{{ value ?? 'null' }}</output>
</p>
<p>
Submitted: <output id="submitted">{{ submitted }}</output>
</p>
</form>
</template>

Angular has a package of its own, @telixon/angular, with a directive that takes part in reactive, template-driven, and Signal Forms. An Angular template still renders the elements once the component declares CUSTOM_ELEMENTS_SCHEMA, which the compiler asks for on any custom element. [options] binds the property and (telixon-change) the event:

import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
@Component({
selector: 'app-phone',
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
<telixon-phone-field
name="phone"
default-region="US"
calling-code-in-input="false"
required
(telixon-change)="onChange($event)"
>
<telixon-region-picker></telixon-region-picker>
<input type="tel" autocomplete="tel" />
</telixon-phone-field>
`,
})
export class PhoneComponent {
onChange(event: Event): void {}
}

defineTelixonElements() belongs in main.ts, which only the browser runs. This component compiles under strictTemplates.

A page without a bundler loads the package from a module script. An import map resolves the package, @telixon/core, @telixon/web-sdk, and @telixon/web-sdk/flags to their files, from a CDN or from the page’s own copies. The sprite images sit next to flags.css and resolve from it:

<script type="importmap">
{
"imports": {
"@telixon/core": "https://cdn.jsdelivr.net/npm/@telixon/core/dist/index.browser.js",
"@telixon/web-sdk": "https://cdn.jsdelivr.net/npm/@telixon/web-sdk/dist/index.js",
"@telixon/web-sdk/flags": "https://cdn.jsdelivr.net/npm/@telixon/web-sdk/dist/flags/index.js",
"@telixon/web-components": "https://cdn.jsdelivr.net/npm/@telixon/web-components/dist/index.js"
}
}
</script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@telixon/web-components/dist/flags/flags.css" />
<script type="module">
import { defineTelixonElements } from '@telixon/web-components';
defineTelixonElements();
</script>