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.
React 19
Section titled “React 19”@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> );}import { defineTelixonElements } from '@telixon/web-components';import '@telixon/web-components/flags/flags.css';import { hydrateRoot } from 'react-dom/client';import { App } from './App';
defineTelixonElements();
const root = document.getElementById('root');if (root === null) throw new Error('The page lacks the root.');hydrateRoot(root, <App />);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>import { defineTelixonElements } from '@telixon/web-components';import '@telixon/web-components/flags/flags.css';import type {} from '@telixon/web-components/vue';import { createApp } from 'vue';import App from './App.vue';
defineTelixonElements();
createApp(App).mount('#app');import react from '@vitejs/plugin-react';import vue from '@vitejs/plugin-vue';import { defineConfig } from 'vite';
// The React page also renders on the server, through a second build of its server entry.export default defineConfig(({ isSsrBuild }) => ({ plugins: [react(), vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('telixon-') } } })], build: isSsrBuild ? { outDir: 'dist/server' } : { outDir: 'dist/client', rolldownOptions: { input: { static: 'index.html', react: 'react/index.html', vue: 'vue/index.html' } }, },}));Angular
Section titled “Angular”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.
Without a bundler
Section titled “Without a bundler”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>