Un Web Component es un elemento HTML personalizado, creado con estándares del navegador (Custom Elements, Shadow DOM y HTML Templates). Permite encapsular HTML + CSS + JS en un único bloque reutilizable, sin depender de frameworks.
¿Dónde se utiliza?
Funciona en cualquier entorno donde exista HTML:
- Aplicaciones web clásicas (PHP, Java, .NET)
- Frameworks modernos (Svelte, React, Vue, Angular)
- CMS (WordPress, Drupal)
- Apps híbridas (Electron, Ionic)
- Sistemas grandes con equipos múltiples
¿Cómo se usa en HTML?
Una vez registrado el componente, usarlo es tan simple como:
<mi-componente titulo="Hola" modo="dark"></mi-componente>
El navegador lo trata como si fuera una etiqueta nativa.
Repositorios de Web Components gratuitos
- WebComponents.org — El mayor catálogo público de Web Components. Reúne componentes de Google, Vaadin, Shoelace, FAST y cientos de desarrolladores independientes.
- Shoelace — Biblioteca moderna de componentes UI (botones, modales, tabs, formularios). 100% Web Components, sin framework y altamente personalizable.
- Vaadin Web Components — Componentes empresariales de alta calidad: grids, formularios, layouts, pickers. Muy usados en aplicaciones corporativas.
- FAST Components — Componentes de Microsoft, rápidos, accesibles y con diseño adaptable. Ideales para apps modernas.
- Elix — Componentes avanzados de UI (carousels, date pickers, menús complejos) con un enfoque en accesibilidad y rendimiento.
- PatternFly Elements — Componentes creados por Red Hat para interfaces profesionales y dashboards.
- Lion Web Components — Componentes minimalistas y accesibles creados por ING, perfectos para formularios y validación.
Objetivo
Una vez que ya sabemos cómo construir Aplicaciones en Svelte 5, analizar los cambios a realizar para desarrollar WEB Components con Svelte 5.
Las funcionalidades que deseamos comprobar:
- Integrarse en páginas HTML de aplicaciones WEB, del lenguaje PHP o ASP, etc.
- Validas para instanciarse varias veces en la misma página (ejemplo páginas de Grid).
- Poder pasar parámetros desde la aplicación WEB al inicio de la ejecución.
- Poder enviar información desde dentro a la página HTML a través de eventos.
- Poder recibir información desde dentro (Svelte 5) como evento, para su tratamiento en página HTML.
DEMO: https://fhumanes.com/pc_001
La imagen muestra un ejemplo de aplicación hecha con PHPRunner con (1) una app de Svelte 5 integrada y con (2) un Web Components hecho en Svelte 5.
Solución Técnica
Ciclo de Vida de WEB Components
Este trabajo se ha realizado con las consultas realizadas a Copilot, por lo que seguramente tú lo podrías hacer, lo único que te facilito es el conocimiento de esto en menos tiempo que lo que tardarías en obtener la información en alguna de las IA’s.
Realmente, una vez que conoces cómo desarrollar aplicaciones en Svelte 5, hacer WEB Components es muy sencillo y las adaptaciones para que tus desarrollos (App completas hechas con Svelte 5) puedas integrarlas en cualquier solución que utilice HTML son pequeñas cosas que hay que conocer y que te voy a explicar.
Hacer un desarrollo de Web Components en Svelte 5.
Se crea un proyecto exactamente igual que si fuera una App (Svelte 5 JavaScript). Puedes avanzar cómo si fuese una aplicación completa y después realizar los cambio que indico o hacer estos cambios desde el principio.
A la izquierda está el proyecto que voy a explicar y los ficheros que a continuación muestro y explico están en la imagen.
Veis que muestro el directorio «dist», que es donde se van a generar los ficheros ( JS y CSS) que utilizaremos en la aplicación PHPRunner (aplicación WEB).
Es posible que a ti no te genere fichero CSS. Svelte 5, genera este tipo de fichero si utilizas ficheros CSS externos en tu proyecto, como el fichero «app.css».
import { mount } from 'svelte';
import App from './App.svelte';
// import './app.css';
class PcPlugin extends HTMLElement {
connectedCallback() {
// Props desde atributos HTML
const props = {
usuario: this.getAttribute('usuario') ?? '',
color: this.getAttribute('color') ?? 'red'
};
// Shadow DOM para aislar estilos (opcional pero recomendable)
this._root = this.attachShadow({ mode: 'open' });
// Instancia Svelte independiente por cada <pc-plugin>
this._app = mount(App, {
target: this._root,
props
});
// MÉTODOS PÚBLICOS
this.setUsuario = (nuevo) => {
this._app.setUsuario(nuevo);
};
this.reset = () => {
this._app.reset();
};
}
disconnectedCallback() {
if (this._app && this._app.destroy) {
this._app.destroy();
}
}
}
// Registrar el custom element UNA sola vez
customElements.define('pc-plugin', PcPlugin);
import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';
export default defineConfig({
plugins: [svelte()],
build: {
outDir: 'dist',
assetsDir: '',
assetsInlineLimit: 0
}
});
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Dev Web Component</title>
</head>
<body>
<h2>Prueba del Web Component</h2>
<pc-plugin id="plugin1" usuario="Fernando" color="red">
</pc-plugin>
<pc-plugin id="plugin2" usuario="Ana" color="blue">
</pc-plugin>
<!-- Cargar el Web Component desde Vite -->
<script type="module" src="/src/main.js"></script>
<script>
document.getElementById("plugin1").addEventListener("contadorChange", (e) => {
console.log("Evento (1):", e.detail);
});
document.getElementById("plugin2").addEventListener("contadorChange", (e) => {
console.log("Evento (2):", e.detail);
});
// Llamadas a métodos públicos del Web Component
setTimeout(() => {
const plugin1 = document.getElementById("plugin1");
const plugin2 = document.getElementById("plugin2");
plugin1.setUsuario("Carlos");
plugin2.setUsuario("María");
}, 1000);
</script>
</body>
</html>
<script>
import { onMount } from 'svelte';
export let usuario = 'Sin nombre';
export let color = 'black';
// Métodos públicos
export function reset() {
usuario = 'Sin nombre';
console.log("Reset ejecutado");
};
export function setUsuario(nuevo) {
usuario = nuevo;
console.log("cambio de ususario a:", nuevo);
}
function evento() {
const msg = 'Se pulsó el botón en el plugin';
// MUY IMPORTANTE: usar "this"
this.dispatchEvent(new CustomEvent("contadorChange", {
detail: { msg },
bubbles: true,
composed: true
}));
}
</script>
<div
style="border:1px solid #ccc; padding:4px; margin:4px; color:{color}">
Hola {usuario}, soy un plugin Svelte 5 en un Web Component.
<button onclick={evento}>Evento</button>
</div>
Aspectos relevantes del código expuesto:
- main.js .- En este fichero está «la gracia» de la adaptación, pues se ha utilizado JavaScript para definir las características del WEB Components.
- En línea 5 se define el nombre «interno» de la clase. Puedes poner el nombre que mejor describa lo que resuelve.
- En línea 8 define los parámetros iniciales que se van a reportar desde el HTML
- En línea 23, se define los métodos públicos o eventos que se van a poder enviar desde el HTML.
- En línea 33. Es una función general que se va a ejecutar cuando la página HTML desaparezca (se cierre o cambie por otra). Hay que dejar este mismo código.
- en línea 41. Aquí se define la relación de Tag del HTML y la Clase, definida en línea 5.
- vite.config.js .- Hay que copiar y pegar el contenido de este fichero.
Lo único relevante es que se ha quitado el directorio de «asset». - index.html .- Este fichero HTML, tiene las características que se pueden definir en HTML, para probar la funcionalidad, de forma completa, del código realizado.
Características a tener en cuenta:- Líneas 10 a 13, se define las 2 instancias del custom elements «pc-plugin» y los parámetros que se pasan al instanciarse.
- Líneas 17 a 23, se define el «escuchador» de los eventos que puede ofrecer el código Svelte.
En el elemento, acceso por «id», se escucha este evento «contadorChange». - Líneas 26 a 31.- Se define un «retardador», para que pasado 1 segundo se procese la función «setUsuario», para cambiar el nombre de usuario de cada instancia.
- App.svelte .- Este es el inicio del código Svelte, pero no hay problema en que se llamen a otros, como en el ejemplo, a los ficheros del directorio «lib», que tiene la configuración de los acceso al server (SLIM 4 – PHP) y que puede acceder a la sesión de la aplicación PHP y a la base de datos del aplicativo. Es muy importante el acceso y actualización de la sesión, puesto que simplifica que estos componente puedan conocer el estado de la aplicación WEB (identificado o no el usuario, perfil, etc.)
En este blog hay muchos ejemplos de funcionalidad que se podría desarrollar de esta forma, pero creo que una alternativa a los informes de PHPRunner puede ser el ejemplo S-013 ( generar informes PDF en el navegador ), donde se puede mostrar el icono de la impresora y al pulsar, se genera (velocidad excepcional) la factura, albarán, nota, etc., que requiera el aplicativo.
Adaptar una aplicación Svelte 5 para integrarla en el HTML
Esta solución puede servir perfectamente para establecer un Dashboard o Cuadro de control de un aplicativo, ya que con Svelte 5, la capacidad de mostrar gráficos, etc, es infinita y muy sencilla. En todas las aplicaciones medianas, grandes, páginas e información de este tipo es obligada y con PHPRunner y productos similares, no es tan sencillo.
El ejemplo «plugin_pc_001» es el que voy a explicar y para ello te muestro los ficheros más relevantes:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>plugin_pc_001</title>
</head>
<body>
<div id="plugin_pc_001"
data-usuario="Invitado_DEV"
data-color="Black">
</div>
<script type="module" src="/src/main.js"></script>
<script>
window.addEventListener("svelteChange", (e) => {
console.log("Valor recibido desde Svelte:", e.detail);
});
setTimeout(() => {
window.plugin_pc_001.setUsuario("Carlos");
}, 4000);
setTimeout(() => {
window.plugin_pc_001.reset();
}, 8000);
</script>
</body>
</html>
import { mount } from 'svelte';
import App from './App.svelte';
const el = document.getElementById('plugin_pc_001');
const app =mount(App, {
target: el,
props: {
usuario: el.dataset.usuario,
color: el.dataset.color
}
});
// Exponer la instancia al exterior
window.plugin_pc_001 = app;
import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
// https://vite.dev/config/
export default defineConfig({
plugins: [svelte()],
base: '',
build: {
outDir: 'dist',
assetsDir: 'assets',
assetsInlineLimit: 0 // Asegura que los assets no se conviertan en base64
}
});
<script>
import { onMount } from 'svelte';
import api from './lib/api.js';
// Props que recibes desde PHP
let { usuario = "Invitado", color = "white" } = $props();
// MÉTODOS PÚBLICOS
export function setUsuario(nuevo) {
console.log('Cambiando usuario a:', nuevo);
usuario = nuevo;
}
export function reset() {
console.log('Reset ejecutado');
usuario = "Sin nombre";
resetearContador(); // Cargar datos al montar
}
// eventos hacia fuera del componente, para que otros componentes puedan escuchar los cambios
function notificarCambio() {
// MUY IMPORTANTE: usar "windows" o "document" para disparar el evento, no "this"
document.dispatchEvent(new CustomEvent("svelteChange", {
detail: { contador },
bubbles: true,
composed: true
}));
}
// Estado interno del componente
let contador = $state(0);
let mensajeError = "";
let loading = $state(true);
let datos = $state([]);
onMount(() => { // Montar el componente
resetearContador(); // Cargar datos al montar
});
// Llamada a tu API Slim 4 usando la cookie de sesión
// recoger los datos de la sesión
async function cargarDatos() {
try {
const res = await api.get(`/session`);
// console.log('Respuesta de la API:', res);
// El array real está aquí:
const array = res.data;
console.log('Array recibido:', array);
datos = array;
contador = res.data.SESSION.contador; // Actualizar el contador con el valor de la sesión
/*
const recordsNormalized = array
.map(({ id_group: id, creation_date, ...rest }) => ({
id,
creation_date: createDateFromMySQL(creation_date),
creation_date_text: formatDateToDDMMYYYY(createDateFromMySQL(creation_date)),
...rest
}));
*/
} catch (err) {
console.error('Error al cargar datos de Sesión:', err);
} finally {
loading = false;
}
}
// Llamada a tu API Slim 4 usando la cookie de sesión
// Incrementar el contador y luego recoger los datos de la sesión
async function incrementoContador() {
console.log('Incrementando contador...');
try {
const res = await api.get(`/updateSession`);
console.log('Respuesta de la API:', res);
// El array real está aquí:
const array = res.data;
console.log('Array recibido (update):', array);
datos = array;
contador = res.data.SESSION.contador; // Actualizar el contador con el valor de la sesión
/*
const recordsNormalized = array
.map(({ id_group: id, creation_date, ...rest }) => ({
id,
creation_date: createDateFromMySQL(creation_date),
creation_date_text: formatDateToDDMMYYYY(createDateFromMySQL(creation_date)),
...rest
}));
*/
} catch (err) {
console.error('Error al cargar datos de Sesión:', err);
} finally {
notificarCambio(); // Notificar el cambio después de la llamada a la API
}
}
// Resetear el contador y luego recoger los datos de la sesión
async function resetearContador() {
console.log('Resetear contador...');
try {
const res = await api.get(`/resetSession`);
console.log('Respuesta de la API:', res);
// El array real está aquí:
const array = res.data;
console.log('Array recibido (update):', array);
datos = array;
contador = res.data?.SESSION?.contador ?? 0; // Actualizar el contador con el valor de la sesión
/*
const recordsNormalized = array
.map(({ id_group: id, creation_date, ...rest }) => ({
id,
creation_date: createDateFromMySQL(creation_date),
creation_date_text: formatDateToDDMMYYYY(createDateFromMySQL(creation_date)),
...rest
}));
*/
} catch (err) {
console.error('Error al cargar datos de Sesión:', err);
} finally {
// incrementar(); // Incrementar el contador después de la llamada a la API
}
}
function incrementar() {
contador++;
}
</script>
<style>
.panel {
padding: 1rem;
border-radius: 8px;
background: #f4f4f4;
border: 1px solid #ccc;
width: 300px;
font-family: sans-serif;
}
.titulo {
font-size: 1.2rem;
font-weight: bold;
margin-bottom: .5rem;
}
button {
margin-top: .5rem;
padding: .4rem .8rem;
border-radius: 4px;
border: none;
background: #0077cc;
color: white;
cursor: pointer;
}
button:hover {
background: #005fa3;
}
</style>
<div class="panel">
<div class="titulo">Panel de {usuario}</div>
<p>Color: <strong>{color}</strong></p>
<p>Contador interno: {contador}</p>
<button onclick={incrementoContador}>Incrementar</button>
<hr />
<button onclick={cargarDatos}>
Cargar datos desde API Slim
</button>
<br/>
<button onclick={resetearContador}>
Resetear contador desde server
</button>
</div>
Aspectos relevantes del código expuesto:
- index.html .- Se adadpta para poder probar toda la funcionalidad que desarrollemos.
- Línea 10 . Por defecto las aplicaciones buscan el objeto con «id» = «app», pero para que disponga de un nombre menos geérico, debemos dar un nombre «nuestro».
- Líneas 11-12.- Así pasamos los parámetros de instanciación del módulo.
- Líneas 16-17. se «escucha» el evento de la App, para que el código interno (Svelte) comunique cambios al HTML.
- Líneas 19-24. Es la operación contraría, desde el HTML se comunica cambios el código Svelte.
- main.js .- Los cambios son pequeños.
- Línea 4 . se expedicifa el nuevo «id» que vamos a utilizar en HTML para identificar dónde va a ir el contenido del código Svelte.
- Líneas 8-11. Se indican los parámetros que vamos a recoger en el momento de instanciar el módulo.
- Línea 15. Se expone el nombre de la instancia al HTML
- vite.config.js . Aquí no hay cambios respecto al desarrollo de una aplicación normal.
- App.svelte .- Sería el inicio de la aplicación de Svelte. Se debe programar estas partes.
- Línea 6 . Se reciben los parámetros del HTML.
- Líneas 8-18. Se definen la fucniones que desde el exterior (HTML) se van a poder utilizar.
- Línea 22-30. Se define el evento que va a interactuar con el DOM, y por lo tanto, con el JavaScript del HTML.
- Líneas 46-133. Utilizando AXIOS ( ajax) mediante las librerías del dicrectorio «lib» nos comunicamos con el server (SLIM 4) , para acceso a las variables de $_SESSION de la aplicación y en este caso, está preparado para acceso a la base de datos, aunque no lo está utilizando.
Para verificar que está funciuonando, hay múltiples traza que se pueden ver en la consola del navegador.
La aplicación de SLIM4 -PHP, llamada «pc_001-server», también la entrego, para que podáis descargar todo en vuestros PC, pero su explicación la haré en otro artículo.
Espero que veáis el gran potecial que se are con las soluciones de WEB Components, más cuando podemos hacerlos a nuestra medida y para lo que necesitéis, podéis contactar conmigo a través del email.